From 2ec358082f0896480bdbfcb289b4ba2bff0dd828 Mon Sep 17 00:00:00 2001 From: Severin Gehwolf Date: Mon, 11 Nov 2024 13:35:25 +0000 Subject: [PATCH] 8311302: Implement JEP 493: Linking Run-Time Images without JMODs Co-authored-by: Mandy Chung Reviewed-by: mchung, alanb, erikj, ihse --- make/Images.gmk | 4 + make/autoconf/jdk-options.m4 | 33 +- make/autoconf/spec.gmk.template | 1 + .../jlink/internal/ImageFileCreator.java | 376 +++++++++- .../jdk/tools/jlink/internal/JRTArchive.java | 525 +++++++++++++ .../jdk/tools/jlink/internal/Jlink.java | 26 +- .../jdk/tools/jlink/internal/JlinkTask.java | 356 +++++---- .../jlink/internal/LinkableRuntimeImage.java | 88 +++ .../jdk/tools/jlink/internal/TaskHelper.java | 38 +- .../runtimelink/JimageDiffGenerator.java | 113 +++ .../internal/runtimelink/ResourceDiff.java | 286 +++++++ .../runtimelink/ResourcePoolReader.java | 57 ++ .../RuntimeImageLinkException.java | 62 ++ .../tools/jlink/resources/jlink.properties | 19 +- test/hotspot/jtreg/TEST.ROOT | 4 +- test/jdk/TEST.ROOT | 4 +- .../jdk/modules/etc/JmodExcludedFiles.java | 1 + .../jdk/tools/jlink/ImageFileCreatorTest.java | 7 +- test/jdk/tools/jlink/IntegrationTest.java | 23 +- .../jlink/JLinkDedupTestBatchSizeOne.java | 31 +- .../tools/jlink/JLinkHelpCapabilityTest.java | 78 ++ .../JLinkMRJavaBaseVersionTest.java | 3 +- .../plugins/GenerateJLIClassesPluginTest.java | 29 +- .../plugins/IncludeLocalesPluginTest.java | 35 +- .../AbstractLinkableRuntimeTest.java | 705 ++++++++++++++++++ .../jlink/runtimeImage/AddOptionsTest.java | 86 +++ .../BasicJlinkMissingJavaBase.java | 72 ++ .../jlink/runtimeImage/BasicJlinkTest.java | 69 ++ .../jlink/runtimeImage/CapturingHandler.java | 42 ++ .../runtimeImage/CustomModuleJlinkTest.java | 84 +++ .../runtimeImage/GenerateJLIClassesTest.java | 89 +++ .../jlink/runtimeImage/JImageHelper.java | 65 ++ .../runtimeImage/JavaSEReproducibleTest.java | 75 ++ .../KeepPackagedModulesFailTest.java | 92 +++ .../runtimeImage/ModifiedFilesExitTest.java | 85 +++ .../jlink/runtimeImage/ModifiedFilesTest.java | 72 ++ .../ModifiedFilesWarningTest.java | 75 ++ .../jlink/runtimeImage/MultiHopTest.java | 93 +++ ...PackagedModulesVsRuntimeImageLinkTest.java | 175 +++++ .../PatchedJDKModuleJlinkTest.java | 124 +++ .../jlink/runtimeImage/SystemModulesTest.java | 81 ++ .../runtimeImage/SystemModulesTest2.java | 76 ++ test/jdk/tools/lib/tests/Helper.java | 27 +- test/jdk/tools/lib/tests/JImageGenerator.java | 15 +- test/jtreg-ext/requires/VMProps.java | 16 + .../tools/javac/plugin/AutostartPlugins.java | 2 +- .../tools/javac/plugin/InternalAPI.java | 2 +- 47 files changed, 4170 insertions(+), 251 deletions(-) create mode 100644 src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java create mode 100644 src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java create mode 100644 src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/JimageDiffGenerator.java create mode 100644 src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourceDiff.java create mode 100644 src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourcePoolReader.java create mode 100644 src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/RuntimeImageLinkException.java create mode 100644 test/jdk/tools/jlink/JLinkHelpCapabilityTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/AbstractLinkableRuntimeTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/AddOptionsTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/BasicJlinkMissingJavaBase.java create mode 100644 test/jdk/tools/jlink/runtimeImage/BasicJlinkTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/CapturingHandler.java create mode 100644 test/jdk/tools/jlink/runtimeImage/CustomModuleJlinkTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/GenerateJLIClassesTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/JImageHelper.java create mode 100644 test/jdk/tools/jlink/runtimeImage/JavaSEReproducibleTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/KeepPackagedModulesFailTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/ModifiedFilesExitTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/ModifiedFilesTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/ModifiedFilesWarningTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/MultiHopTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/PatchedJDKModuleJlinkTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/SystemModulesTest.java create mode 100644 test/jdk/tools/jlink/runtimeImage/SystemModulesTest2.java diff --git a/make/Images.gmk b/make/Images.gmk index 10fc8041325..acdc594b009 100644 --- a/make/Images.gmk +++ b/make/Images.gmk @@ -96,6 +96,10 @@ JLINK_DISABLE_WARNINGS := | ( $(GREP) -v -e "WARNING: Using incubator module" || JDK_IMAGE_SUPPORT_DIR := $(SUPPORT_OUTPUTDIR)/images/jdk JRE_IMAGE_SUPPORT_DIR := $(SUPPORT_OUTPUTDIR)/images/jre +ifeq ($(JLINK_PRODUCE_LINKABLE_RUNTIME), true) + JLINK_JDK_EXTRA_OPTS += --generate-linkable-runtime +endif + $(eval $(call SetupExecute, jlink_jdk, \ WARN := Creating jdk image, \ DEPS := $(JDK_JMODS) $(BASE_RELEASE_FILE) \ diff --git a/make/autoconf/jdk-options.m4 b/make/autoconf/jdk-options.m4 index c5c2290019b..cf8a856de96 100644 --- a/make/autoconf/jdk-options.m4 +++ b/make/autoconf/jdk-options.m4 @@ -586,13 +586,42 @@ AC_DEFUN_ONCE([JDKOPT_SETUP_JMOD_OPTIONS], ################################################################################ # # jlink options. -# We always keep packaged modules in JDK image. # AC_DEFUN_ONCE([JDKOPT_SETUP_JLINK_OPTIONS], [ - UTIL_ARG_ENABLE(NAME: keep-packaged-modules, DEFAULT: true, + + ################################################################################ + # + # Configure option for building a JDK that is suitable for linking from the + # run-time image without JMODs. + # + # Determines whether or not a suitable run-time image is being produced from + # packaged modules. If set to 'true, changes the *default* of packaged + # modules to 'false'. + # + UTIL_ARG_ENABLE(NAME: linkable-runtime, DEFAULT: false, + RESULT: JLINK_PRODUCE_LINKABLE_RUNTIME, + DESC: [enable a JDK build suitable for linking from the run-time image], + CHECKING_MSG: [whether or not a JDK suitable for linking from the run-time image should be produced]) + AC_SUBST(JLINK_PRODUCE_LINKABLE_RUNTIME) + + if test "x$JLINK_PRODUCE_LINKABLE_RUNTIME" = xtrue; then + DEFAULT_PACKAGED_MODULES=false + else + DEFAULT_PACKAGED_MODULES=true + fi + + ################################################################################ + # + # Configure option for packaged modules + # + # We keep packaged modules in the JDK image unless --enable-linkable-runtime is + # requested. + # + UTIL_ARG_ENABLE(NAME: keep-packaged-modules, DEFAULT: $DEFAULT_PACKAGED_MODULES, RESULT: JLINK_KEEP_PACKAGED_MODULES, DESC: [enable keeping of packaged modules in jdk image], + DEFAULT_DESC: [enabled by default unless --enable-linkable-runtime is set], CHECKING_MSG: [if packaged modules are kept]) AC_SUBST(JLINK_KEEP_PACKAGED_MODULES) ]) diff --git a/make/autoconf/spec.gmk.template b/make/autoconf/spec.gmk.template index 231355043d5..bcd54058c28 100644 --- a/make/autoconf/spec.gmk.template +++ b/make/autoconf/spec.gmk.template @@ -706,6 +706,7 @@ NEW_JAVADOC = $(INTERIM_LANGTOOLS_ARGS) $(JAVADOC_MAIN_CLASS) JMOD_COMPRESS := @JMOD_COMPRESS@ JLINK_KEEP_PACKAGED_MODULES := @JLINK_KEEP_PACKAGED_MODULES@ +JLINK_PRODUCE_LINKABLE_RUNTIME := @JLINK_PRODUCE_LINKABLE_RUNTIME@ RCFLAGS := @RCFLAGS@ diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java index 5664c195003..466c5d0a14c 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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 @@ -24,29 +24,44 @@ */ package jdk.tools.jlink.internal; +import static jdk.tools.jlink.internal.LinkableRuntimeImage.DIFF_PATTERN; +import static jdk.tools.jlink.internal.LinkableRuntimeImage.RESPATH_PATTERN; + import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.tools.jlink.internal.Archive.Entry; import jdk.tools.jlink.internal.Archive.Entry.EntryType; +import jdk.tools.jlink.internal.JRTArchive.ResourceFileEntry; import jdk.tools.jlink.internal.ResourcePoolManager.CompressedModuleData; +import jdk.tools.jlink.internal.runtimelink.JimageDiffGenerator; +import jdk.tools.jlink.internal.runtimelink.JimageDiffGenerator.ImageResource; +import jdk.tools.jlink.internal.runtimelink.ResourceDiff; +import jdk.tools.jlink.internal.runtimelink.ResourcePoolReader; +import jdk.tools.jlink.internal.runtimelink.RuntimeImageLinkException; import jdk.tools.jlink.plugin.PluginException; import jdk.tools.jlink.plugin.ResourcePool; +import jdk.tools.jlink.plugin.ResourcePoolBuilder; import jdk.tools.jlink.plugin.ResourcePoolEntry; +import jdk.tools.jlink.plugin.ResourcePoolModule; /** * An image (native endian.) @@ -68,38 +83,61 @@ import jdk.tools.jlink.plugin.ResourcePoolEntry; * } */ public final class ImageFileCreator { + private static final byte[] EMPTY_RESOURCE_BYTES = new byte[] {}; + + private static final String JLINK_MOD_NAME = "jdk.jlink"; + private static final String RESPATH = "/" + JLINK_MOD_NAME + "/" + RESPATH_PATTERN; + private static final String DIFF_PATH = "/" + JLINK_MOD_NAME + "/" + DIFF_PATTERN; private final Map> entriesForModule = new HashMap<>(); private final ImagePluginStack plugins; - private ImageFileCreator(ImagePluginStack plugins) { + private final boolean generateRuntimeImage; + private final TaskHelper helper; + + private ImageFileCreator(ImagePluginStack plugins, + boolean generateRuntimeImage, + TaskHelper taskHelper) { this.plugins = Objects.requireNonNull(plugins); + this.generateRuntimeImage = generateRuntimeImage; + this.helper = taskHelper; } - public static ExecutableImage create(Set archives, - ImagePluginStack plugins) - throws IOException { - return ImageFileCreator.create(archives, ByteOrder.nativeOrder(), - plugins); - } - - public static ExecutableImage create(Set archives, - ByteOrder byteOrder) - throws IOException { - return ImageFileCreator.create(archives, byteOrder, - new ImagePluginStack()); - } - + /** + * Create an executable image based on a set of input archives and a given + * plugin stack for a given byte order. It optionally generates a runtime + * that can be used for linking from the run-time image if + * {@code generateRuntimeImage} is set to {@code true}. + * + * @param archives The set of input archives + * @param byteOrder The desired byte order of the result + * @param plugins The plugin stack to apply to the input + * @param generateRuntimeImage if a runtime suitable for linking from the + * run-time image should get created. + * @return The executable image. + * @throws IOException + */ public static ExecutableImage create(Set archives, ByteOrder byteOrder, - ImagePluginStack plugins) + ImagePluginStack plugins, + boolean generateRuntimeImage, + TaskHelper taskHelper) throws IOException { - ImageFileCreator image = new ImageFileCreator(plugins); + ImageFileCreator image = new ImageFileCreator(plugins, + generateRuntimeImage, + taskHelper); try { image.readAllEntries(archives); // write to modular image image.writeImage(archives, byteOrder); + } catch (RuntimeImageLinkException e) { + // readAllEntries() might throw this exception. + // Propagate as IOException with appropriate message for + // jlink runs from the run-time image. This handles better + // error messages for the case of modified files in the run-time + // image. + throw image.newIOException(e); } finally { - //Close all archives + // Close all archives for (Archive a : archives) { a.close(); } @@ -125,7 +163,8 @@ public final class ImageFileCreator { public static void recreateJimage(Path jimageFile, Set archives, - ImagePluginStack pluginSupport) + ImagePluginStack pluginSupport, + boolean generateRuntimeImage) throws IOException { try { Map> entriesForModule @@ -142,7 +181,7 @@ public final class ImageFileCreator { try (OutputStream fos = Files.newOutputStream(jimageFile); BufferedOutputStream bos = new BufferedOutputStream(fos); DataOutputStream out = new DataOutputStream(bos)) { - generateJImage(pool, writer, pluginSupport, out); + generateJImage(pool, writer, pluginSupport, out, generateRuntimeImage); } } finally { //Close all archives @@ -158,9 +197,14 @@ public final class ImageFileCreator { BasicImageWriter writer = new BasicImageWriter(byteOrder); ResourcePoolManager allContent = createPoolManager(archives, entriesForModule, byteOrder, writer); - ResourcePool result; + ResourcePool result = null; try (DataOutputStream out = plugins.getJImageFileOutputStream()) { - result = generateJImage(allContent, writer, plugins, out); + result = generateJImage(allContent, writer, plugins, out, generateRuntimeImage); + } catch (RuntimeImageLinkException e) { + // Propagate as IOException with appropriate message for + // jlink runs from the run-time image. This handles better + // error messages for the case of --patch-module. + throw newIOException(e); } //Handle files. @@ -174,14 +218,53 @@ public final class ImageFileCreator { } } + private IOException newIOException(RuntimeImageLinkException e) throws IOException { + if (JlinkTask.DEBUG) { + e.printStackTrace(); + } + String message = switch (e.getReason()) { + case PATCH_MODULE -> helper.getMessage("err.runtime.link.patched.module", e.getFile()); + case MODIFIED_FILE -> helper.getMessage("err.runtime.link.modified.file", e.getFile()); + default -> throw new AssertionError("Unexpected value: " + e.getReason()); + }; + throw new IOException(message); + } + + /** + * Create a jimage based on content of the given ResourcePoolManager, + * optionally creating a runtime that can be used for linking from the + * run-time image + * + * @param allContent The content that needs to get added to the resulting + * lib/modules (jimage) file. + * @param writer The writer for the jimage file. + * @param pluginSupport The stack of all plugins to apply. + * @param out The output stream to write the jimage to. + * @param generateRuntimeImage if a runtime suitable for linking from the + * run-time image should get created. + * @return A pool of the actual result resources. + * @throws IOException + */ private static ResourcePool generateJImage(ResourcePoolManager allContent, BasicImageWriter writer, ImagePluginStack pluginSupport, - DataOutputStream out + DataOutputStream out, + boolean generateRuntimeImage ) throws IOException { ResourcePool resultResources; try { resultResources = pluginSupport.visitResources(allContent); + if (generateRuntimeImage) { + // Keep track of non-modules resources for linking from a run-time image + resultResources = addNonClassResourcesTrackFiles(resultResources, + writer); + // Generate the diff between the input resources from packaged + // modules in 'allContent' to the plugin- or otherwise + // generated-content in 'resultResources' + resultResources = addResourceDiffFiles(allContent.resourcePool(), + resultResources, + writer); + } } catch (PluginException pe) { if (JlinkTask.DEBUG) { pe.printStackTrace(); @@ -198,7 +281,7 @@ public final class ImageFileCreator { List content = new ArrayList<>(); List paths = new ArrayList<>(); - // the order of traversing the resources and the order of + // the order of traversing the resources and the order of // the module content being written must be the same resultResources.entries().forEach(res -> { if (res.type().equals(ResourcePoolEntry.Type.CLASS_OR_RESOURCE)) { @@ -248,11 +331,225 @@ public final class ImageFileCreator { return resultResources; } + /** + * Support for creating a runtime suitable for linking from the run-time + * image. + * + * Generates differences between the packaged modules "view" in + * {@code jmodContent} to the optimized image in {@code resultContent} and + * adds the result to the returned resource pool. + * + * @param jmodContent The resource pool view of packaged modules + * @param resultContent The optimized result generated from the jmodContent + * input by applying the plugin stack. + * @param writer The image writer. + * @return The resource pool with the difference file resources added to + * the {@code resultContent} + */ + @SuppressWarnings("try") + private static ResourcePool addResourceDiffFiles(ResourcePool jmodContent, + ResourcePool resultContent, + BasicImageWriter writer) { + JimageDiffGenerator generator = new JimageDiffGenerator(); + List diff; + try (ImageResource jmods = new ResourcePoolReader(jmodContent); + ImageResource jimage = new ResourcePoolReader(resultContent)) { + diff = generator.generateDiff(jmods, jimage); + } catch (Exception e) { + throw new AssertionError("Failed to generate the runtime image diff", e); + } + Set modules = resultContent.moduleView().modules() + .map(a -> a.name()) + .collect(Collectors.toSet()); + // Add resource diffs for the resource files we are about to add + modules.stream().forEach(m -> { + String resourceName = String.format(DIFF_PATH, m); + ResourceDiff.Builder builder = new ResourceDiff.Builder(); + ResourceDiff d = builder.setKind(ResourceDiff.Kind.ADDED) + .setName(resourceName) + .build(); + diff.add(d); + }); + Map> perModDiffs = preparePerModuleDiffs(diff, + modules); + return addDiffResourcesFiles(modules, perModDiffs, resultContent, writer); + } + + private static Map> preparePerModuleDiffs(List resDiffs, + Set modules) { + Map> modToDiff = new HashMap<>(); + resDiffs.forEach(d -> { + int secondSlash = d.getName().indexOf("/", 1); + if (secondSlash == -1) { + throw new AssertionError("Module name not present"); + } + String module = d.getName().substring(1, secondSlash); + List perModDiff = modToDiff.computeIfAbsent(module, + a -> new ArrayList<>()); + perModDiff.add(d); + }); + Map> allModsToDiff = new HashMap<>(); + modules.stream().forEach(m -> { + List d = modToDiff.get(m); + if (d == null) { + // Not all modules will have a diff + allModsToDiff.put(m, Collections.emptyList()); + } else { + allModsToDiff.put(m, d); + } + }); + return allModsToDiff; + } + + private static ResourcePool addDiffResourcesFiles(Set modules, + Map> perModDiffs, + ResourcePool resultResources, + BasicImageWriter writer) { + ResourcePoolManager mgr = createPoolManager(resultResources, writer); + ResourcePoolBuilder out = mgr.resourcePoolBuilder(); + modules.stream().sorted().forEach(module -> { + String mResource = String.format(DIFF_PATH, module); + List diff = perModDiffs.get(module); + // Note that for modules without diff to the packaged modules view + // we create resource diff files with just the header and no content. + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try { + ResourceDiff.write(diff, bout); + } catch (IOException e) { + throw new AssertionError("Failed to write resource diff file" + + " for module " + module, e); + } + out.add(ResourcePoolEntry.create(mResource, bout.toByteArray())); + }); + return out.build(); + } + + /** + * Support for creating runtimes that can be used for linking from the + * run-time image. Adds meta-data files for resources not in the lib/modules + * file of the JDK. That is, mapping files for which on-disk files belong to + * which module. + * + * @param resultResources + * The original resources which serve as the basis for generating + * the meta-data files. + * @param writer + * The image writer. + * + * @return An amended resource pool which includes meta-data files. + */ + private static ResourcePool addNonClassResourcesTrackFiles(ResourcePool resultResources, + BasicImageWriter writer) { + // Only add resources if jdk.jlink module is present in the target image + Optional jdkJlink = resultResources.moduleView() + .findModule(JLINK_MOD_NAME); + if (jdkJlink.isPresent()) { + Map> nonClassResources = recordAndFilterEntries(resultResources); + return addModuleResourceEntries(resultResources, nonClassResources, writer); + } else { + return resultResources; // No-op + } + } + + /** + * Support for creating runtimes that can be used for linking from the + * run-time image. Adds the given mapping of files as a meta-data file to + * the given resource pool. + * + * @param resultResources + * The resource pool to add files to. + * @param nonClassResEntries + * The per module mapping for which to create the meta-data files + * for. + * @param writer + * The image writer. + * + * @return A resource pool with meta-data files added. + */ + private static ResourcePool addModuleResourceEntries(ResourcePool resultResources, + Map> nonClassResEntries, + BasicImageWriter writer) { + Set inputModules = resultResources.moduleView().modules() + .map(rm -> rm.name()) + .collect(Collectors.toSet()); + ResourcePoolManager mgr = createPoolManager(resultResources, writer); + ResourcePoolBuilder out = mgr.resourcePoolBuilder(); + inputModules.stream().sorted().forEach(module -> { + String mResource = String.format(RESPATH, module); + List mResources = nonClassResEntries.get(module); + if (mResources == null) { + // We create empty resource files for modules in the resource + // pool view that don't themselves contain native resources + // or config files. + out.add(ResourcePoolEntry.create(mResource, EMPTY_RESOURCE_BYTES)); + } else { + String mResContent = mResources.stream().sorted() + .collect(Collectors.joining("\n")); + out.add(ResourcePoolEntry.create(mResource, + mResContent.getBytes(StandardCharsets.UTF_8))); + } + }); + return out.build(); + } + + /** + * Support for creating runtimes that can be used for linking from the + * run-time image. Generates a per module mapping of files not part of the + * modules image (jimage). This mapping is needed so as to know which files + * of the installed JDK belong to which module. + * + * @param resultResources + * The resources from which the mapping gets generated + * @return A mapping with the module names as keys and the list of files not + * part of the modules image (jimage) as values. + */ + private static Map> recordAndFilterEntries(ResourcePool resultResources) { + Map> nonClassResEntries = new HashMap<>(); + Platform platform = getTargetPlatform(resultResources); + resultResources.entries().forEach(entry -> { + // Note that the fs_$module_files file is a resource file itself, so + // we cannot add fs_$module_files themselves due to the + // not(class_or_resources) condition. However, we also don't want + // to track 'release' file entries (not(top) condition) as those are + // handled by the release info plugin. + if (entry.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE && + entry.type() != ResourcePoolEntry.Type.TOP) { + List mRes = nonClassResEntries.computeIfAbsent(entry.moduleName(), + a -> new ArrayList<>()); + ResourceFileEntry rfEntry = ResourceFileEntry.toResourceFileEntry(entry, + platform); + mRes.add(rfEntry.encodeToString()); + } + }); + return nonClassResEntries; + } + + private static Platform getTargetPlatform(ResourcePool in) { + String platform = in.moduleView().findModule("java.base") + .map(ResourcePoolModule::targetPlatform) + .orElseThrow(() -> new AssertionError("java.base not found")); + return Platform.parsePlatform(platform); + } + private static ResourcePoolManager createPoolManager(Set archives, Map> entriesForModule, ByteOrder byteOrder, BasicImageWriter writer) throws IOException { - ResourcePoolManager resources = new ResourcePoolManager(byteOrder, new StringTable() { + ResourcePoolManager resources = createBasicResourcePoolManager(byteOrder, writer); + archives.stream() + .map(Archive::moduleName) + .sorted() + .flatMap(mn -> + entriesForModule.get(mn).stream() + .map(e -> new ArchiveEntryResourcePoolEntry(mn, + e.getResourcePoolEntryName(), e))) + .forEach(resources::add); + return resources; + } + + private static ResourcePoolManager createBasicResourcePoolManager(ByteOrder byteOrder, + BasicImageWriter writer) { + return new ResourcePoolManager(byteOrder, new StringTable() { @Override public int addString(String str) { @@ -264,14 +561,25 @@ public final class ImageFileCreator { return writer.getString(id); } }); - archives.stream() - .map(Archive::moduleName) - .sorted() - .flatMap(mn -> - entriesForModule.get(mn).stream() - .map(e -> new ArchiveEntryResourcePoolEntry(mn, - e.getResourcePoolEntryName(), e))) - .forEach(resources::add); + } + + /** + * Creates a ResourcePoolManager from existing resources so that more + * resources can be appended. + * + * @param resultResources The existing resources to initially add. + * @param writer The basic image writer. + * @return An appendable ResourcePoolManager. + */ + private static ResourcePoolManager createPoolManager(ResourcePool resultResources, + BasicImageWriter writer) { + ResourcePoolManager resources = createBasicResourcePoolManager(resultResources.byteOrder(), + writer); + // Note that resources are already sorted in the correct order. + // The underlying ResourcePoolManager keeps track of entries via + // LinkedHashMap, which keeps values in insertion order. Therefore + // adding resources here, preserving that same order is OK. + resultResources.entries().forEach(resources::add); return resources; } 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 new file mode 100644 index 00000000000..755afea8c60 --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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.tools.jlink.internal; + +import static jdk.tools.jlink.internal.LinkableRuntimeImage.RESPATH_PATTERN; +import static jdk.tools.jlink.internal.runtimelink.RuntimeImageLinkException.Reason.MODIFIED_FILE; +import static jdk.tools.jlink.internal.runtimelink.RuntimeImageLinkException.Reason.PATCH_MODULE; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +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.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.internal.util.OperatingSystem; +import jdk.tools.jlink.internal.Archive.Entry.EntryType; +import jdk.tools.jlink.internal.runtimelink.ResourceDiff; +import jdk.tools.jlink.internal.runtimelink.RuntimeImageLinkException; +import jdk.tools.jlink.plugin.ResourcePoolEntry; +import jdk.tools.jlink.plugin.ResourcePoolEntry.Type; + +/** + * An archive implementation based on the JDK's run-time image. That is, classes + * and resources from the modules image (lib/modules, or jimage) and other + * 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; + // 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. + // Thus, native libraries, binaries, legal files, etc. + private final List otherRes; + // Maps a module resource path to the corresponding diff to packaged + // modules for that resource (if any) + private final Map resDiff; + private final boolean errorOnModifiedFile; + private final TaskHelper taskHelper; + + /** + * JRTArchive constructor + * + * @param module The module name this archive refers to + * @param path The JRT filesystem path. + * @param errorOnModifiedFile Whether or not modified files of the JDK + * install aborts the link. + * @param perModDiff The lib/modules (a.k.a jimage) diff for this module, + * possibly an empty list if there are no differences. + */ + JRTArchive(String module, + Path path, + boolean errorOnModifiedFile, + List perModDiff, + TaskHelper taskHelper) { + this.module = module; + this.path = path; + this.ref = ModuleFinder.ofSystem() + .find(module) + .orElseThrow(() -> + new IllegalArgumentException( + "Module " + module + + " not part of the JDK install")); + this.errorOnModifiedFile = errorOnModifiedFile; + this.otherRes = readModuleResourceFile(module); + this.resDiff = Objects.requireNonNull(perModDiff).stream() + .collect(Collectors.toMap(ResourceDiff::getName, Function.identity())); + this.taskHelper = taskHelper; + } + + @Override + public String moduleName() { + return module; + } + + @Override + public Path getPath() { + return path; + } + + @Override + public Stream entries() { + try { + collectFiles(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return files.stream().map(JRTFile::toEntry); + } + + @Override + public void open() throws IOException { + if (files.isEmpty()) { + collectFiles(); + } + } + + @Override + public void close() throws IOException { + if (!files.isEmpty()) { + files.clear(); + } + } + + @Override + public int hashCode() { + return Objects.hash(module, path); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof JRTArchive other && + Objects.equals(module, other.module) && + Objects.equals(path, other.path)); + } + + 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()); + // 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()); + } + } + + /* + * no need to keep track of the warning produced since this is eagerly + * checked once. + */ + private void addNonClassResources() { + // Not all modules will have other resources like bin, lib, legal etc. + // files. In that case the list will be empty. + if (!otherRes.isEmpty()) { + files.addAll(otherRes.stream() + .filter(Predicate.not(String::isEmpty)) + .map(s -> { + ResourceFileEntry m = ResourceFileEntry.decodeFromString(s); + + // Read from the base JDK image. + Path path = BASE.resolve(m.resPath); + if (shaSumMismatch(path, m.hashOrTarget, m.symlink)) { + if (errorOnModifiedFile) { + throw new RuntimeImageLinkException(path.toString(), MODIFIED_FILE); + } else { + taskHelper.warning("err.runtime.link.modified.file", path.toString()); + } + } + + return new JRTArchiveFile(JRTArchive.this, + m.resPath, + toEntryType(m.resType), + m.hashOrTarget, + m.symlink, + /* diff only for resources */ + null); + }) + .toList()); + } + } + + static boolean shaSumMismatch(Path res, String expectedSha, boolean isSymlink) { + if (isSymlink) { + return false; + } + // handle non-symlink resources + try { + HexFormat format = HexFormat.of(); + byte[] expected = format.parseHex(expectedSha); + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + try (InputStream is = Files.newInputStream(res)) { + byte[] buf = new byte[1024]; + int readBytes = -1; + while ((readBytes = is.read(buf)) != -1) { + digest.update(buf, 0, readBytes); + } + } + byte[] actual = digest.digest(); + return !MessageDigest.isEqual(expected, actual); + } catch (Exception e) { + throw new AssertionError("SHA-512 sum check failed!", e); + } + } + + private static EntryType toEntryType(Type input) { + return switch(input) { + case CLASS_OR_RESOURCE -> EntryType.CLASS_OR_RESOURCE; + case CONFIG -> EntryType.CONFIG; + case HEADER_FILE -> EntryType.HEADER_FILE; + case LEGAL_NOTICE -> EntryType.LEGAL_NOTICE; + case MAN_PAGE -> EntryType.MAN_PAGE; + case NATIVE_CMD -> EntryType.NATIVE_CMD; + case NATIVE_LIB -> EntryType.NATIVE_LIB; + case TOP -> throw new IllegalArgumentException( + "TOP files should be handled by ReleaseInfoPlugin!"); + default -> throw new IllegalArgumentException("Unknown type: " + input); + }; + } + + public record ResourceFileEntry(Type resType, + boolean symlink, + String hashOrTarget, + String resPath) { + // Type file format: + // '|{0,1}||' + // (1) (2) (3) (4) + // + // Where fields are: + // + // (1) The resource type as specified by ResourcePoolEntry.type() + // (2) Symlink designator. 0 => regular resource, 1 => symlinked resource + // (3) The SHA-512 sum of the resources' content. The link to the target + // for symlinked resources. + // (4) The relative file path of the resource + private static final String TYPE_FILE_FORMAT = "%d|%d|%s|%s"; + + private static final Map typeMap = Arrays.stream(Type.values()) + .collect(Collectors.toMap(Type::ordinal, Function.identity())); + + public String encodeToString() { + return String.format(TYPE_FILE_FORMAT, + resType.ordinal(), + symlink ? 1 : 0, + hashOrTarget, + 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 + * a hash sum (sha512) of the file denoted by the fourth token (path). + */ + static ResourceFileEntry decodeFromString(String line) { + assert !line.isEmpty(); + + String[] tokens = line.split("\\|", 4); + Type type = null; + int symlinkNum = -1; + try { + Integer typeInt = Integer.valueOf(tokens[0]); + type = typeMap.get(typeInt); + if (type == null) { + throw new AssertionError("Illegal type ordinal: " + typeInt); + } + symlinkNum = Integer.valueOf(tokens[1]); + } catch (NumberFormatException e) { + throw new AssertionError(e); // must not happen + } + if (symlinkNum < 0 || symlinkNum > 1) { + throw new AssertionError( + "Symlink designator out of range [0,1] got: " + + symlinkNum); + } + return new ResourceFileEntry(type, + symlinkNum == 1, + tokens[2] /* hash or target */, + tokens[3] /* resource path */); + } + + public static ResourceFileEntry toResourceFileEntry(ResourcePoolEntry entry, + Platform platform) { + String resPathWithoutMod = dropModuleFromPath(entry, platform); + // Symlinks don't have a hash sum, but a link to the target instead + String hashOrTarget = entry.linkedTarget() == null + ? computeSha512(entry) + : dropModuleFromPath(entry.linkedTarget(), + platform); + return new ResourceFileEntry(entry.type(), + entry.linkedTarget() != null, + hashOrTarget, + resPathWithoutMod); + } + + private static String computeSha512(ResourcePoolEntry entry) { + try { + assert entry.linkedTarget() == null; + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + try (InputStream is = entry.content()) { + byte[] buf = new byte[1024]; + int bytesRead = -1; + while ((bytesRead = is.read(buf)) != -1) { + digest.update(buf, 0, bytesRead); + } + } + byte[] db = digest.digest(); + HexFormat format = HexFormat.of(); + return format.formatHex(db); + } catch (Exception e) { + throw new AssertionError("Failed to generate hash sum for " + + entry.path()); + } + } + + private static String dropModuleFromPath(ResourcePoolEntry entry, + Platform platform) { + String resPath = entry.path() + .substring( + // + 2 => prefixed and suffixed '/' + // For example: '/java.base/' + entry.moduleName().length() + 2); + if (!isWindows(platform)) { + return resPath; + } + // For Windows the libraries live in the 'bin' folder rather than + // the 'lib' folder in the final image. Note that going by the + // NATIVE_LIB type only is insufficient since only files with suffix + // .dll/diz/map/pdb are transplanted to 'bin'. + // See: DefaultImageBuilder.nativeDir() + return nativeDir(entry, resPath); + } + + private static boolean isWindows(Platform platform) { + return platform.os() == OperatingSystem.WINDOWS; + } + + private static String nativeDir(ResourcePoolEntry entry, String resPath) { + if (entry.type() != ResourcePoolEntry.Type.NATIVE_LIB) { + return resPath; + } + // precondition: Native lib, windows platform + if (resPath.endsWith(".dll") || resPath.endsWith(".diz") + || resPath.endsWith(".pdb") || resPath.endsWith(".map")) { + if (resPath.startsWith(LIB_DIRNAME + "/")) { + return BIN_DIRNAME + "/" + + resPath.substring((LIB_DIRNAME + "/").length()); + } + } + return resPath; + } + private static final String BIN_DIRNAME = "bin"; + private static final String LIB_DIRNAME = "lib"; + } + + private static final Path BASE = Paths.get(System.getProperty("java.home")); + + interface JRTFile { + Entry toEntry(); + } + + record JRTArchiveFile(Archive archive, + String resPath, + EntryType resType, + String sha, + boolean symlink, + ResourceDiff diff) implements JRTFile { + public Entry toEntry() { + 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. + try { + return Files.size(archive.getPath().resolve(resPath)); + } catch (NoSuchFileException file) { + // This indicates that we don't find the class in the + // modules image using the JRT FS provider. Yet, we find + // the class using the system module finder. Therefore, + // we have a patched module. Mention that module patching + // is not supported. + throw new RuntimeImageLinkException(file.getFile(), PATCH_MODULE); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @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(); + } + } + + }; + } + } + + private static List readModuleResourceFile(String modName) { + String resName = String.format(RESPATH_PATTERN, modName); + try { + try (InputStream inStream = JRTArchive.class.getModule() + .getResourceAsStream(resName)) { + String input = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); + if (input.isEmpty()) { + // Not all modules have non-class resources + return Collections.emptyList(); + } else { + return Arrays.asList(input.split("\n")); + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to process resources from the " + + "run-time image for module " + modName, e); + } + } +} diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Jlink.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Jlink.java index 1c5b0a3cd57..465a1cae8d9 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Jlink.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Jlink.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 @@ -26,7 +26,6 @@ package jdk.tools.jlink.internal; import java.lang.module.Configuration; import java.lang.module.ModuleFinder; -import java.nio.ByteOrder; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -148,6 +147,9 @@ public final class Jlink { private final Path output; private final Set modules; private final ModuleFinder finder; + private final boolean linkFromRuntimeImage; + private final boolean ignoreModifiedRuntime; + private final boolean generateRuntimeImage; /** * jlink configuration, @@ -158,10 +160,16 @@ public final class Jlink { */ public JlinkConfiguration(Path output, Set modules, - ModuleFinder finder) { + ModuleFinder finder, + boolean linkFromRuntimeImage, + boolean ignoreModifiedRuntime, + boolean generateRuntimeImage) { this.output = output; this.modules = Objects.requireNonNull(modules); this.finder = finder; + this.linkFromRuntimeImage = linkFromRuntimeImage; + this.ignoreModifiedRuntime = ignoreModifiedRuntime; + this.generateRuntimeImage = generateRuntimeImage; } /** @@ -186,6 +194,18 @@ public final class Jlink { return finder; } + public boolean linkFromRuntimeImage() { + return linkFromRuntimeImage; + } + + public boolean ignoreModifiedRuntime() { + return ignoreModifiedRuntime; + } + + public boolean isGenerateRuntimeImage() { + return generateRuntimeImage; + } + /** * Returns a {@link Configuration} of the given module path, * root modules with full service binding. 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 d9dbf1d0661..15998d6b929 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, 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 @@ -24,6 +24,8 @@ */ package jdk.tools.jlink.internal; +import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE; + import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; @@ -39,14 +41,15 @@ import java.lang.module.ResolutionException; import java.lang.module.ResolvedModule; import java.net.URI; import java.nio.ByteOrder; -import java.nio.file.Files; import java.nio.file.FileVisitResult; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; @@ -60,18 +63,18 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import jdk.internal.module.ModulePath; import jdk.internal.module.ModuleReferenceImpl; -import jdk.tools.jlink.internal.TaskHelper.BadArgs; -import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE; +import jdk.internal.module.ModuleResolution; +import jdk.internal.opt.CommandLine; +import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider; import jdk.tools.jlink.internal.Jlink.JlinkConfiguration; import jdk.tools.jlink.internal.Jlink.PluginsConfiguration; +import jdk.tools.jlink.internal.TaskHelper.BadArgs; import jdk.tools.jlink.internal.TaskHelper.Option; import jdk.tools.jlink.internal.TaskHelper.OptionsHelper; -import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider; +import jdk.tools.jlink.internal.runtimelink.RuntimeImageLinkException; import jdk.tools.jlink.plugin.PluginException; -import jdk.internal.opt.CommandLine; -import jdk.internal.module.ModulePath; -import jdk.internal.module.ModuleResolution; /** * Implementation for the jlink tool. @@ -86,7 +89,6 @@ public class JlinkTask { private static final TaskHelper taskHelper = new TaskHelper(JLINK_BUNDLE); - private static final Option[] recognizedOptions = { new Option(false, (task, opt, arg) -> { task.options.help = true; @@ -182,7 +184,17 @@ public class JlinkTask { }, true, "--full-version"), new Option(false, (task, opt, arg) -> { task.options.ignoreSigning = true; - }, "--ignore-signing-information"),}; + }, "--ignore-signing-information"), + new Option(false, (task, opt, arg) -> { + task.options.ignoreModifiedRuntime = true; + }, true, "--ignore-modified-runtime"), + // option for generating a runtime that can then + // be used for linking from the run-time image. + new Option(false, (task, opt, arg) -> { + task.options.generateLinkableRuntime = true; + }, true, "--generate-linkable-runtime") + }; + private static final String PROGNAME = "jlink"; private final OptionsValues options = new OptionsValues(); @@ -222,6 +234,8 @@ public class JlinkTask { boolean ignoreSigning = false; boolean bindServices = false; boolean suggestProviders = false; + boolean ignoreModifiedRuntime = false; + boolean generateLinkableRuntime = false; } public static final String OPTIONS_RESOURCE = "jdk/tools/jlink/internal/options"; @@ -252,7 +266,7 @@ public class JlinkTask { .showUsage(true); } if (options.help) { - optionsHelper.showHelp(PROGNAME); + optionsHelper.showHelp(PROGNAME, LinkableRuntimeImage.isLinkableRuntime()); return EXIT_OK; } if (optionsHelper.shouldListPlugins()) { @@ -270,11 +284,6 @@ public class JlinkTask { if (jmods != null) { options.modulePath.add(jmods); } - - if (options.modulePath.isEmpty()) { - throw taskHelper.newBadArgs("err.modulepath.must.be.specified") - .showUsage(true); - } } JlinkConfiguration config = initJlinkConfig(); @@ -300,7 +309,7 @@ public class JlinkTask { } cleanupOutput(outputPath); return EXIT_ERROR; - } catch (IllegalArgumentException | ResolutionException e) { + } catch (IllegalArgumentException | ResolutionException | RuntimeImageLinkException e) { log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage()); if (DEBUG) { e.printStackTrace(log); @@ -356,6 +365,7 @@ public class JlinkTask { false, null, false, + new OptionsValues(), null); // Then create the Plugin Stack @@ -370,7 +380,7 @@ public class JlinkTask { private JlinkConfiguration initJlinkConfig() throws BadArgs { Set roots = new HashSet<>(); for (String mod : options.addMods) { - if (mod.equals(ALL_MODULE_PATH)) { + if (mod.equals(ALL_MODULE_PATH) && options.modulePath.size() > 0) { ModuleFinder finder = newModuleFinder(options.modulePath, options.limitMods, Set.of()); // all observable modules are roots finder.findAll() @@ -392,9 +402,75 @@ public class JlinkTask { finder = newModuleFinder(options.modulePath, options.limitMods, roots); } + boolean isLinkFromRuntime = options.modulePath.isEmpty(); + // In case of custom modules outside the JDK we may + // have a non-empty module path, which must not include + // java.base. If it did, we link using packaged modules from that + // module path. If the module path does not include java.base, we have + // the case where we link from the run-time image. In that case, we take + // the JDK modules from the run-time image (ModuleFinder.ofSystem()). + if (finder.find("java.base").isEmpty()) { + isLinkFromRuntime = true; + ModuleFinder runtimeImageFinder = ModuleFinder.ofSystem(); + finder = combinedFinders(runtimeImageFinder, finder, options.limitMods, roots); + } + + // --keep-packaged-modules doesn't make sense as we are not linking + // from packaged modules to begin with. + if (isLinkFromRuntime && options.packagedModulesPath != null) { + throw taskHelper.newBadArgs("err.runtime.link.packaged.mods"); + } + return new JlinkConfiguration(options.output, roots, - finder); + finder, + isLinkFromRuntime, + options.ignoreModifiedRuntime, + options.generateLinkableRuntime); + } + + /** + * Creates a combined module finder of {@code finder} and + * {@code runtimeImageFinder} that first looks-up modules in the + * {@code runtimeImageFinder} and if not present in {@code finder}. + * + * @param runtimeImageFinder A system modules finder. + * @param finder A module finder based on packaged modules. + * @param limitMods The set of limited modules for the resulting + * finder (if any). + * @param roots All module roots. + * + * @return A combined finder, or the input finder, potentially applying + * module limits. + */ + private ModuleFinder combinedFinders(ModuleFinder runtimeImageFinder, + ModuleFinder finder, + Set limitMods, + Set roots) { + ModuleFinder combined = new ModuleFinder() { + + @Override + public Optional find(String name) { + Optional mref = runtimeImageFinder.find(name); + if (mref.isEmpty()) { + return finder.find(name); + } + return mref; + } + + @Override + public Set findAll() { + Set all = new HashSet<>(); + all.addAll(runtimeImageFinder.findAll()); + all.addAll(finder.findAll()); + return Collections.unmodifiableSet(all); + } + }; + // if limitmods is specified then limit the universe + if (limitMods != null && !limitMods.isEmpty()) { + return limitFinder(combined, limitMods, Objects.requireNonNull(roots)); + } + return combined; } private void createImage(JlinkConfiguration config) throws Exception { @@ -413,6 +489,7 @@ public class JlinkTask { options.bindServices, options.endian, options.verbose, + options, log); // Then create the Plugin Stack @@ -433,10 +510,10 @@ public class JlinkTask { } /* - * Returns a module finder of the given module path that limits - * the observable modules to those in the transitive closure of - * the modules specified in {@code limitMods} plus other modules - * specified in the {@code roots} set. + * Returns a module finder of the given module path or the system modules + * if the module path is empty that limits the observable modules to those + * in the transitive closure of the modules specified in {@code limitMods} + * plus other modules specified in the {@code roots} set. * * @throws IllegalArgumentException if java.base module is present * but its descriptor has no version @@ -445,14 +522,10 @@ public class JlinkTask { Set limitMods, Set roots) { - if (Objects.requireNonNull(paths).isEmpty()) { - throw new IllegalArgumentException(taskHelper.getMessage("err.empty.module.path")); - } - - Path[] entries = paths.toArray(new Path[0]); Runtime.Version version = Runtime.version(); - ModuleFinder finder = ModulePath.of(version, true, entries); - + Path[] entries = paths.toArray(new Path[0]); + ModuleFinder finder = paths.isEmpty() ? ModuleFinder.ofSystem() + : ModulePath.of(version, true, entries); if (finder.find("java.base").isPresent()) { // use the version of java.base module, if present, as // the release version for multi-release JAR files @@ -505,8 +578,9 @@ public class JlinkTask { private static Path toPathLocation(ResolvedModule m) { Optional ouri = m.reference().location(); - if (ouri.isEmpty()) + if (ouri.isEmpty()) { throw new InternalError(m + " does not have a location"); + } URI uri = ouri.get(); return Paths.get(uri); } @@ -518,6 +592,7 @@ public class JlinkTask { boolean bindService, ByteOrder endian, boolean verbose, + OptionsValues opts, PrintWriter log) throws IOException { @@ -534,12 +609,35 @@ public class JlinkTask { taskHelper.getMessage("err.automatic.module", mref.descriptor().name(), loc)); }); + // Perform some sanity checks for linking from the run-time image + if (config.linkFromRuntimeImage()) { + if (!LinkableRuntimeImage.isLinkableRuntime()) { + String msg = taskHelper.getMessage("err.runtime.link.not.linkable.runtime"); + throw new IllegalArgumentException(msg); + } + // Do not permit linking from run-time image and also including jdk.jlink module + if (cf.findModule(JlinkTask.class.getModule().getName()).isPresent()) { + String msg = taskHelper.getMessage("err.runtime.link.jdk.jlink.prohibited"); + throw new IllegalArgumentException(msg); + } + + // Print info message indicating linking from the run-time image + if (verbose && log != null) { + log.println(taskHelper.getMessage("runtime.link.info")); + } + } + if (verbose && log != null) { // print modules to be linked in cf.modules().stream() .sorted(Comparator.comparing(ResolvedModule::name)) - .forEach(rm -> log.format("%s %s%n", - rm.name(), rm.reference().location().get())); + .forEach(rm -> log.format("%s %s%s%n", + rm.name(), + rm.reference().location().get(), + // We have a link from run-time image when scheme is 'jrt' + "jrt".equals(rm.reference().location().get().getScheme()) + ? " " + taskHelper.getMessage("runtime.link.jprt.path.extra") + : "")); // print provider info Set references = cf.modules().stream() @@ -559,14 +657,15 @@ public class JlinkTask { .map(ModuleDescriptor::name) .collect(Collectors.joining(", ")); - if (!"".equals(im)) + if (!"".equals(im)) { log.println("WARNING: Using incubator modules: " + im); + } } Map mods = cf.modules().stream() .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation)); // determine the target platform of the image being created - Platform targetPlatform = targetPlatform(cf, mods); + Platform targetPlatform = targetPlatform(cf, mods, config.linkFromRuntimeImage()); // if the user specified any --endian, then it must match the target platform's native // endianness if (endian != null && endian != targetPlatform.arch().byteOrder()) { @@ -580,7 +679,92 @@ public class JlinkTask { targetPlatform.arch().byteOrder(), targetPlatform); } } - return new ImageHelper(cf, mods, targetPlatform, retainModulesPath, ignoreSigning); + + // use the version of java.base module, if present, as + // the release version for multi-release JAR files + var version = cf.findModule("java.base") + .map(ResolvedModule::reference) + .map(ModuleReference::descriptor) + .flatMap(ModuleDescriptor::version) + .map(ModuleDescriptor.Version::toString) + .map(Runtime.Version::parse) + .orElse(Runtime.version()); + + Set archives = mods.entrySet().stream() + .map(e -> newArchive(e.getKey(), + e.getValue(), + version, + ignoreSigning, + config, + log)) + .collect(Collectors.toSet()); + + return new ImageHelper(archives, + targetPlatform, + retainModulesPath, + config.isGenerateRuntimeImage()); + } + + private static Archive newArchive(String module, + Path path, + Runtime.Version version, + boolean ignoreSigning, + JlinkConfiguration config, + PrintWriter log) { + if (path.toString().endsWith(".jmod")) { + return new JmodArchive(module, path); + } else if (path.toString().endsWith(".jar")) { + ModularJarArchive modularJarArchive = new ModularJarArchive(module, path, version); + try (Stream entries = modularJarArchive.entries()) { + boolean hasSignatures = entries.anyMatch((entry) -> { + String name = entry.name().toUpperCase(Locale.ROOT); + + return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && ( + name.endsWith(".SF") || + name.endsWith(".DSA") || + name.endsWith(".RSA") || + name.endsWith(".EC") || + name.startsWith("META-INF/SIG-") + ); + }); + + if (hasSignatures) { + if (ignoreSigning) { + System.err.println(taskHelper.getMessage("warn.signing", path)); + } else { + throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path)); + } + } + } + return modularJarArchive; + } else if (Files.isDirectory(path) && !"jrt".equals(path.toUri().getScheme())) { + // The jrt URI path scheme conditional is there since we'd otherwise + // enter this branch for linking from the run-time image where the + // path is a jrt path. Note that the specific module would be a + // directory. I.e. Files.isDirectory() would be true. + Path modInfoPath = path.resolve("module-info.class"); + if (Files.isRegularFile(modInfoPath)) { + return new DirArchive(path, findModuleName(modInfoPath)); + } else { + throw new IllegalArgumentException( + taskHelper.getMessage("err.not.a.module.directory", path)); + } + } else if (config.linkFromRuntimeImage()) { + return LinkableRuntimeImage.newArchive(module, path, config.ignoreModifiedRuntime(), taskHelper); + } else { + throw new IllegalArgumentException( + taskHelper.getMessage("err.not.modular.format", module, path)); + } + } + + private static String findModuleName(Path modInfoPath) { + try (BufferedInputStream bis = new BufferedInputStream( + Files.newInputStream(modInfoPath))) { + return ModuleDescriptor.read(bis).name(); + } catch (IOException exp) { + throw new IllegalArgumentException(taskHelper.getMessage( + "err.cannot.read.module.info", modInfoPath), exp); + } } /* @@ -626,10 +810,12 @@ public class JlinkTask { }; } - private static Platform targetPlatform(Configuration cf, Map modsPaths) throws IOException { + private static Platform targetPlatform(Configuration cf, + Map modsPaths, + boolean runtimeImageLink) throws IOException { Path javaBasePath = modsPaths.get("java.base"); assert javaBasePath != null : "java.base module path is missing"; - if (isJavaBaseFromDefaultModulePath(javaBasePath)) { + if (runtimeImageLink || isJavaBaseFromDefaultModulePath(javaBasePath)) { // this implies that the java.base module used for the target image // will correspond to the current platform. So this isn't an attempt to // build a cross-platform image. We use the current platform's endianness @@ -720,8 +906,9 @@ public class JlinkTask { String header, Set modules, Map> serviceToUses) { - if (modules.isEmpty()) + if (modules.isEmpty()) { return; + } // Build a map of a service type to the provider modules Map> providers = new HashMap<>(); @@ -845,95 +1032,14 @@ public class JlinkTask { return sb.toString(); } - private static class ImageHelper implements ImageProvider { - final Platform targetPlatform; - final Path packagedModulesPath; - final boolean ignoreSigning; - final Runtime.Version version; - final Set archives; - - ImageHelper(Configuration cf, - Map modsPaths, - Platform targetPlatform, - Path packagedModulesPath, - boolean ignoreSigning) throws IOException { - Objects.requireNonNull(targetPlatform); - this.targetPlatform = targetPlatform; - this.packagedModulesPath = packagedModulesPath; - this.ignoreSigning = ignoreSigning; - - // use the version of java.base module, if present, as - // the release version for multi-release JAR files - this.version = cf.findModule("java.base") - .map(ResolvedModule::reference) - .map(ModuleReference::descriptor) - .flatMap(ModuleDescriptor::version) - .map(ModuleDescriptor.Version::toString) - .map(Runtime.Version::parse) - .orElse(Runtime.version()); - - this.archives = modsPaths.entrySet().stream() - .map(e -> newArchive(e.getKey(), e.getValue())) - .collect(Collectors.toSet()); - } - - private Archive newArchive(String module, Path path) { - if (path.toString().endsWith(".jmod")) { - return new JmodArchive(module, path); - } else if (path.toString().endsWith(".jar")) { - ModularJarArchive modularJarArchive = new ModularJarArchive(module, path, version); - - try (Stream entries = modularJarArchive.entries()) { - boolean hasSignatures = entries.anyMatch((entry) -> { - String name = entry.name().toUpperCase(Locale.ROOT); - - return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && ( - name.endsWith(".SF") || - name.endsWith(".DSA") || - name.endsWith(".RSA") || - name.endsWith(".EC") || - name.startsWith("META-INF/SIG-") - ); - }); - - if (hasSignatures) { - if (ignoreSigning) { - System.err.println(taskHelper.getMessage("warn.signing", path)); - } else { - throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path)); - } - } - } - - return modularJarArchive; - } else if (Files.isDirectory(path)) { - Path modInfoPath = path.resolve("module-info.class"); - if (Files.isRegularFile(modInfoPath)) { - return new DirArchive(path, findModuleName(modInfoPath)); - } else { - throw new IllegalArgumentException( - taskHelper.getMessage("err.not.a.module.directory", path)); - } - } else { - throw new IllegalArgumentException( - taskHelper.getMessage("err.not.modular.format", module, path)); - } - } - - private static String findModuleName(Path modInfoPath) { - try (BufferedInputStream bis = new BufferedInputStream( - Files.newInputStream(modInfoPath))) { - return ModuleDescriptor.read(bis).name(); - } catch (IOException exp) { - throw new IllegalArgumentException(taskHelper.getMessage( - "err.cannot.read.module.info", modInfoPath), exp); - } - } - + private static record ImageHelper(Set archives, + Platform targetPlatform, + Path packagedModulesPath, + boolean generateRuntimeImage) implements ImageProvider { @Override public ExecutableImage retrieve(ImagePluginStack stack) throws IOException { ExecutableImage image = ImageFileCreator.create(archives, - targetPlatform.arch().byteOrder(), stack); + targetPlatform.arch().byteOrder(), stack, generateRuntimeImage, taskHelper); if (packagedModulesPath != null) { // copy the packaged modules to the given path Files.createDirectories(packagedModulesPath); diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java new file mode 100644 index 00000000000..935af4585ad --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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.tools.jlink.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.List; + +import jdk.tools.jlink.internal.runtimelink.ResourceDiff; + +/** + * Class that supports the feature of running jlink based on the current + * run-time image. + */ +public class LinkableRuntimeImage { + + // meta-data files per module for supporting linking from the run-time image + public static final String RESPATH_PATTERN = "jdk/tools/jlink/internal/runtimelink/fs_%s_files"; + // The diff files per module for supporting linking from the run-time image + public static final String DIFF_PATTERN = "jdk/tools/jlink/internal/runtimelink/diff_%s"; + + /** + * In order to be able to show whether or not a runtime is capable of + * linking from it in {@code jlink --help} we need to look for the delta + * files in the {@code jdk.jlink} module. If present we have the capability. + * + * @return {@code true} iff this jlink is capable of linking from the + * run-time image. + */ + public static boolean isLinkableRuntime() { + try (InputStream in = getDiffInputStream("java.base")) { + return in != null; + } catch (IOException e) { + // fall-through + } + return false; + } + + private static InputStream getDiffInputStream(String module) throws IOException { + String resourceName = String.format(DIFF_PATTERN, module); + return LinkableRuntimeImage.class.getModule().getResourceAsStream(resourceName); + } + + public static Archive newArchive(String module, + Path path, + boolean ignoreModifiedRuntime, + TaskHelper taskHelper) { + assert isLinkableRuntime(); + // Here we retrieve the per module difference file, which is + // potentially empty, from the modules image and pass that on to + // JRTArchive for further processing. When streaming resources from + // the archive, the diff is being applied. + List perModuleDiff = null; + try (InputStream in = getDiffInputStream(module)){ + perModuleDiff = ResourceDiff.read(in); + } catch (IOException e) { + throw new AssertionError("Failure to retrieve resource diff for " + + "module " + module, e); + } + return new JRTArchive(module, path, !ignoreModifiedRuntime, perModuleDiff, taskHelper); + } + + +} diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java index 2b4e6ca0a97..23b3dfb7079 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/TaskHelper.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 @@ -28,23 +28,21 @@ import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.text.MessageFormat; -import java.util.Map; -import java.util.HashMap; -import java.util.Map.Entry; -import java.util.Set; -import java.util.HashSet; -import java.util.List; import java.util.ArrayList; import java.util.Arrays; -import java.util.stream.Stream; import java.util.Collections; -import java.util.Locale; -import java.util.ResourceBundle; -import java.util.MissingResourceException; import java.util.Comparator; - +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.stream.Stream; import jdk.tools.jlink.builder.DefaultImageBuilder; import jdk.tools.jlink.builder.ImageBuilder; @@ -55,7 +53,6 @@ import jdk.tools.jlink.internal.plugins.ExcludeJmodSectionPlugin; import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; import jdk.tools.jlink.plugin.Plugin; import jdk.tools.jlink.plugin.Plugin.Category; -import jdk.tools.jlink.plugin.PluginException; /** * @@ -584,7 +581,7 @@ public final class TaskHelper { return null; } - public void showHelp(String progName) { + public void showHelp(String progName, boolean linkableRuntimeEnabled) { log.println(bundleHelper.getMessage("main.usage", progName)); Stream.concat(options.stream(), pluginOptions.mainOptions.stream()) .filter(option -> !option.isHidden()) @@ -594,6 +591,17 @@ public final class TaskHelper { }); log.println(bundleHelper.getMessage("main.command.files")); + // If the JDK build has the run-time image capability show it + // in the help output in human readable form. + String qualifier = null; + if (linkableRuntimeEnabled) { + qualifier = bundleHelper.getMessage("main.runtime.image.linking.cap.enabled"); + } else { + qualifier = bundleHelper.getMessage("main.runtime.image.linking.cap.disabled"); + } + log.println(bundleHelper.getMessage("main.runtime.image.linking.cap.sect.header")); + log.println(bundleHelper.getMessage("main.runtime.image.linking.cap.msg", + qualifier)); } public void listPlugins() { diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/JimageDiffGenerator.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/JimageDiffGenerator.java new file mode 100644 index 00000000000..5e540be7ced --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/JimageDiffGenerator.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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.tools.jlink.internal.runtimelink; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Generates a delta between packaged modules (as an ImageResource) and an + * optimized jimage (lib/modules) as an ImageResource. The result can be + * serialized to a file using {@link ResourceDiff}. + */ +public class JimageDiffGenerator { + + /** + * A resource used for linking. Either packaged modules or + * packaged modules transformed to an optimized run-time image by applying + * the jlink plug-in pipeline. The canonical source, the packaged modules, + * are being used to devise the delta to the transformed run-time image. The + * delta can can then be used for jlink input together *with* a prepared + * run-time image. + */ + @SuppressWarnings("try") + public interface ImageResource extends AutoCloseable { + public List getEntries(); + public byte[] getResourceBytes(String name); + } + + /** + * Produce a difference between packaged modules' resources (base) and the + * result of all plug-ins being applied on those resources (image). + * + * @param base + * The ImageResource view of unmodified resources coming from + * packaged modules. + * @param image + * The ImageResource view of the jlink plug-in pipeline having + * been applied to the resources in base. + * @return The list of resource differences across all modules. + */ + public List generateDiff(ImageResource base, ImageResource image) throws Exception { + List baseResources; + Set resources = new HashSet<>(); + List diffs = new ArrayList<>(); + try (base; image) { + resources.addAll(image.getEntries()); + baseResources = base.getEntries(); + for (String item: baseResources) { + byte[] baseBytes = base.getResourceBytes(item); + // First check that every item in the base image exist in + // the optimized image as well. If it does not, it's a removed + // item in the optimized image. + if (!resources.remove(item)) { + // keep track of original bytes for removed item in the + // optimized image, since we need to restore them for the + // runtime image link + ResourceDiff.Builder builder = new ResourceDiff.Builder(); + ResourceDiff diff = builder.setKind(ResourceDiff.Kind.REMOVED) + .setName(item) + .setResourceBytes(baseBytes) + .build(); + diffs.add(diff); + continue; + } + // Verify resource bytes are equal if present in both images + boolean contentEquals = Arrays.equals(baseBytes, image.getResourceBytes(item)); + if (!contentEquals) { + // keep track of original bytes (non-optimized) + ResourceDiff.Builder builder = new ResourceDiff.Builder(); + ResourceDiff diff = builder.setKind(ResourceDiff.Kind.MODIFIED) + .setName(item) + .setResourceBytes(baseBytes) + .build(); + diffs.add(diff); + } + } + } + // What's now left in the set are the resources only present in the + // optimized image (generated by some plugins; not present in jmods) + for (String e: resources) { + ResourceDiff.Builder builder = new ResourceDiff.Builder(); + ResourceDiff diff = builder.setKind(ResourceDiff.Kind.ADDED) + .setName(e) + .build(); + diffs.add(diff); + } + return diffs; + } + +} diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourceDiff.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourceDiff.java new file mode 100644 index 00000000000..a007350c16f --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourceDiff.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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.tools.jlink.internal.runtimelink; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Class representing a difference of a jimage resource. For all intents + * and purposes this represents a difference between a resource in an optimized + * jimage (e.g. images/jdk/lib/modules) and the underlying basic resources from + * which the optimized image got derived from (e.g. packaged modules). The + * differences are being used in JRTArchive so as to back-track from an optimized + * jimage to the original (i.e. it restores original resources using the diff). + */ +public class ResourceDiff implements Comparable { + + private static final int MAGIC = 0xabba; + + public static enum Kind { + ADDED((short)1), // Resource added + REMOVED((short)2), // Resource removed + MODIFIED((short)3); // Resource modified + + private short value; + + private Kind(short value) { + this.value = value; + } + + public short value() { + return value; + } + + static Kind fromShort(short v) { + if (v > 3 || v < 1) { + throw new IllegalArgumentException("Must be within range [1-3]"); + } + switch (v) { + case 1: return ADDED; + case 2: return REMOVED; + case 3: return MODIFIED; + } + throw new AssertionError("Must not reach here!"); + } + } + + private final Kind kind; + private final byte[] resourceBytes; + private final String name; + + private ResourceDiff(Kind kind, String name, byte[] resourceBytes) { + this.kind = kind; + this.name = name; + if ((kind == Kind.REMOVED || kind == Kind.MODIFIED) && + resourceBytes == null) { + throw new AssertionError("Resource bytes must be set for REMOVED or MODIFIED"); + } + this.resourceBytes = resourceBytes; + } + + public Kind getKind() { + return kind; + } + + public byte[] getResourceBytes() { + return resourceBytes; + } + + public String getName() { + return name; + } + + @Override + public int compareTo(ResourceDiff o) { + int kindComp = kind.value() - o.kind.value(); + if (kindComp == 0) { + return getName().compareTo(o.getName()); + } else { + return kindComp; + } + } + + public static class Builder { + private Kind kind; + private String name; + private byte[] resourceBytes; + + public Builder setKind(Kind kind) { + this.kind = kind; + return this; + } + public Builder setName(String name) { + this.name = Objects.requireNonNull(name); + return this; + } + public Builder setResourceBytes(byte[] resourceBytes) { + this.resourceBytes = Objects.requireNonNull(resourceBytes); + return this; + } + public ResourceDiff build() { + if (kind == null || name == null) { + throw new AssertionError("kind and name must be set"); + } + switch (kind) { + case ADDED: + { + break; // null bytes for added is OK. + } + case MODIFIED: // fall-through + case REMOVED: + { + if (resourceBytes == null) { + throw new AssertionError("Original bytes needed for MODIFIED, REMOVED!"); + } + break; + } + default: + break; + } + return new ResourceDiff(kind, name, resourceBytes); + } + } + + /** + * Writes a list of resource diffs to an output stream + * + * @param diffs The list of resource diffs to write. + * @param out The stream to write the serialized bytes to. + */ + public static void write(List diffs, OutputStream out) throws IOException { + /* + * Simple binary format: + * + *
| + * + * **************************************** + * HEADER info + * **************************************** + * + * where
is ('|' separation for clarity): + * + * | + * + * The first integer is the MAGIC, 0xabba. The second integer is the + * total number of items. + * + * ***************************************** + * ITEMS info + * ***************************************** + * + * Each consists of ('|' separation for clarity): + * + * |||| + * + * Where the individual items are: + * + * : + * The value of the respective ResourceDiff.Kind. + * : + * The length of the name bytes (in UTF-8). + * : + * The resource name bytes in UTF-8. + * : + * The length of the resource bytes. 0 (zero) if no resource bytes. + * A.k.a 'null'. + * : + * The bytes of the resource as stored in the jmod files. + */ + try (DataOutputStream dataOut = new DataOutputStream(out)) { + dataOut.writeInt(MAGIC); + dataOut.writeInt(diffs.size()); + for (ResourceDiff d: diffs) { + dataOut.writeShort(d.kind.value()); + byte[] buf = d.name.getBytes(StandardCharsets.UTF_8); + dataOut.writeInt(buf.length); + dataOut.write(buf); + buf = d.resourceBytes; + dataOut.writeInt(buf == null ? 0 : buf.length); + if (buf != null) { + dataOut.write(buf); + } + } + } + } + + /** + * Read a list of resource diffs from an input stream. + * + * @param in The input stream to read from + * @return The list of resource diffs. + */ + public static List read(InputStream in) throws IOException { + /* + * See write() for the details how this is being written + */ + List diffs = new ArrayList<>(); + try (DataInputStream din = new DataInputStream(in)) { + int magic = din.readInt(); + if (magic != MAGIC) { + throw new IllegalArgumentException("Not a ResourceDiff data stream!"); + } + int numItems = din.readInt(); + for (int i = 0; i < numItems; i++) { + Kind k = Kind.fromShort(din.readShort()); + int numBytes = din.readInt(); + byte[] buf = readBytesFromStream(din, numBytes); + String name = new String(buf, StandardCharsets.UTF_8); + numBytes = din.readInt(); + byte[] resBytes = null; + if (numBytes != 0) { + resBytes = readBytesFromStream(din, numBytes); + } + Builder builder = new Builder(); + builder.setKind(k) + .setName(name); + if (resBytes != null) { + builder.setResourceBytes(resBytes); + } + diffs.add(builder.build()); + } + } + return Collections.unmodifiableList(diffs); + } + + private static byte[] readBytesFromStream(DataInputStream din, int numBytes) throws IOException { + byte[] b = new byte[numBytes]; + for (int i = 0; i < numBytes; i++) { + int data = din.read(); + if (data == -1) { + throw new IOException("Short read!"); + } + b[i] = (byte)data; + } + return b; + } + + public static void printDiffs(List diffs) { + for (ResourceDiff diff: diffs.stream().sorted().toList()) { + switch (diff.getKind()) { + case ADDED: + System.out.println("Only added in opt: " + diff.getName()); + break; + case MODIFIED: + System.out.println("Modified in opt: " + diff.getName()); + break; + case REMOVED: + System.out.println("Removed in opt: " + diff.getName()); + break; + default: + break; + } + } + } + +} diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourcePoolReader.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourcePoolReader.java new file mode 100644 index 00000000000..12e8708477c --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/ResourcePoolReader.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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.tools.jlink.internal.runtimelink; + +import java.util.List; +import java.util.Objects; + +import jdk.tools.jlink.internal.runtimelink.JimageDiffGenerator.ImageResource; +import jdk.tools.jlink.plugin.ResourcePool; +import jdk.tools.jlink.plugin.ResourcePoolEntry; + +@SuppressWarnings("try") +public class ResourcePoolReader implements ImageResource { + + private final ResourcePool pool; + + public ResourcePoolReader(ResourcePool pool) { + this.pool = Objects.requireNonNull(pool); + } + + @Override + public void close() throws Exception { + // nothing + } + + @Override + public List getEntries() { + return pool.entries().map(ResourcePoolEntry::path).toList(); + } + + @Override + public byte[] getResourceBytes(String name) { + return pool.findEntry(name).orElseThrow().contentBytes(); + } + +} diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/RuntimeImageLinkException.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/RuntimeImageLinkException.java new file mode 100644 index 00000000000..9f54fd63476 --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/RuntimeImageLinkException.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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.tools.jlink.internal.runtimelink; + +import java.util.Objects; + +/** + * Exception thrown when linking from the run-time image + */ +public class RuntimeImageLinkException extends RuntimeException { + + private static final long serialVersionUID = -1848914673073119403L; + + public static enum Reason { + PATCH_MODULE, /* link exception due to patched module */ + MODIFIED_FILE, /* link exception due to modified file */ + } + + private final String file; + private final Reason reason; + + public RuntimeImageLinkException(String file, Reason reason) { + this.file = Objects.requireNonNull(file); + this.reason = Objects.requireNonNull(reason); + } + + public String getFile() { + return file; + } + + public Reason getReason() { + return reason; + } + + @Override + public String getMessage() { + return reason + ", file: " + file; + } +} diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties index e09eadbb3d0..9e18177d9c8 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/jlink.properties @@ -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 @@ -111,10 +111,22 @@ main.extended.help.footer=\ \ used, one pattern per line\n\ \n\ +main.runtime.image.linking.cap.enabled=enabled +main.runtime.image.linking.cap.disabled=disabled +main.runtime.image.linking.cap.sect.header=Capabilities: +main.runtime.image.linking.cap.msg=\ Linking from run-time image {0} error.prefix=Error: warn.prefix=Warning: +err.runtime.link.not.linkable.runtime=This JDK does not support linking from the current run-time image +err.runtime.link.jdk.jlink.prohibited=This JDK does not contain packaged modules\ +\ and cannot be used to create another image with the jdk.jlink module +err.runtime.link.packaged.mods=This JDK has no packaged modules.\ +\ --keep-packaged-modules is not supported +err.runtime.link.modified.file={0} has been modified +err.runtime.link.patched.module=File {0} not found in the modules image.\ +\ --patch-module is not supported when linking from the run-time image err.empty.module.path=empty module path err.jlink.version.mismatch=jlink version {0}.{1} does not match target java.base version {2}.{3} err.automatic.module:automatic module cannot be used with jlink: {0} from {1} @@ -123,7 +135,7 @@ err.launcher.main.class.empty:launcher main class name cannot be empty: {0} err.launcher.module.name.empty:launcher module name cannot be empty: {0} err.launcher.value.format:launcher value should be of form =[/]: {0} err.output.must.be.specified:--output must be specified -err.modulepath.must.be.specified:--module-path is not specified and this runtime image does not contain jmods directory. +err.modulepath.must.be.specified:--module-path is not specified and this run-time image does not contain a jmods directory err.mods.must.be.specified:no modules specified to {0} err.path.not.found=path not found: {0} err.path.not.valid=invalid path: {0} @@ -157,3 +169,6 @@ warn.provider.notfound=No provider found for service specified to --suggest-prov no.suggested.providers=--bind-services option is specified. No additional providers suggested. suggested.providers.header=Suggested providers providers.header=Providers + +runtime.link.info=Linking based on the current run-time image +runtime.link.jprt.path.extra=(run-time image) diff --git a/test/hotspot/jtreg/TEST.ROOT b/test/hotspot/jtreg/TEST.ROOT index 21c5aebaa71..af97ff465de 100644 --- a/test/hotspot/jtreg/TEST.ROOT +++ b/test/hotspot/jtreg/TEST.ROOT @@ -86,7 +86,9 @@ requires.properties= \ vm.flagless \ container.support \ systemd.support \ - jdk.containerized + jdk.containerized \ + jlink.runtime.linkable \ + jlink.packagedModules # Minimum jtreg version requiredVersion=7.4+1 diff --git a/test/jdk/TEST.ROOT b/test/jdk/TEST.ROOT index 6276932afbd..7b6276c7aa0 100644 --- a/test/jdk/TEST.ROOT +++ b/test/jdk/TEST.ROOT @@ -102,7 +102,9 @@ requires.properties= \ systemd.support \ release.implementor \ jdk.containerized \ - jdk.foreign.linker + jdk.foreign.linker \ + jlink.runtime.linkable \ + jlink.packagedModules # Minimum jtreg version requiredVersion=7.4+1 diff --git a/test/jdk/jdk/modules/etc/JmodExcludedFiles.java b/test/jdk/jdk/modules/etc/JmodExcludedFiles.java index 6c338a25b4f..90ca6840d52 100644 --- a/test/jdk/jdk/modules/etc/JmodExcludedFiles.java +++ b/test/jdk/jdk/modules/etc/JmodExcludedFiles.java @@ -25,6 +25,7 @@ * @test * @bug 8159927 * @modules java.base/jdk.internal.util + * @requires jlink.packagedModules * @run main JmodExcludedFiles * @summary Test that JDK JMOD files do not include native debug symbols */ diff --git a/test/jdk/tools/jlink/ImageFileCreatorTest.java b/test/jdk/tools/jlink/ImageFileCreatorTest.java index 84f77340f42..b6466c6a4d9 100644 --- a/test/jdk/tools/jlink/ImageFileCreatorTest.java +++ b/test/jdk/tools/jlink/ImageFileCreatorTest.java @@ -34,11 +34,12 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Stream; + +import jdk.tools.jlink.builder.ImageBuilder; import jdk.tools.jlink.internal.Archive; +import jdk.tools.jlink.internal.ExecutableImage; import jdk.tools.jlink.internal.ImageFileCreator; import jdk.tools.jlink.internal.ImagePluginStack; -import jdk.tools.jlink.internal.ExecutableImage; -import jdk.tools.jlink.builder.ImageBuilder; import jdk.tools.jlink.plugin.ResourcePool; @@ -223,6 +224,6 @@ public class ImageFileCreatorTest { ImagePluginStack stack = new ImagePluginStack(noopBuilder, Collections.emptyList(), null, false); - ImageFileCreator.create(archives, ByteOrder.nativeOrder(), stack); + ImageFileCreator.create(archives, ByteOrder.nativeOrder(), stack, false, null); } } diff --git a/test/jdk/tools/jlink/IntegrationTest.java b/test/jdk/tools/jlink/IntegrationTest.java index e85d8f0d984..686dd194ada 100644 --- a/test/jdk/tools/jlink/IntegrationTest.java +++ b/test/jdk/tools/jlink/IntegrationTest.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 @@ -25,7 +25,6 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.UncheckedIOException; -import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -38,20 +37,18 @@ import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.function.Function; -import jdk.tools.jlink.internal.Jlink; -import jdk.tools.jlink.internal.JlinkTask; + import jdk.tools.jlink.builder.DefaultImageBuilder; -import jdk.tools.jlink.internal.Platform; -import jdk.tools.jlink.plugin.ResourcePool; -import jdk.tools.jlink.plugin.ResourcePoolBuilder; -import jdk.tools.jlink.plugin.Plugin; import jdk.tools.jlink.internal.ExecutableImage; +import jdk.tools.jlink.internal.Jlink; import jdk.tools.jlink.internal.Jlink.JlinkConfiguration; import jdk.tools.jlink.internal.Jlink.PluginsConfiguration; +import jdk.tools.jlink.internal.JlinkTask; +import jdk.tools.jlink.internal.Platform; import jdk.tools.jlink.internal.PostProcessor; -import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin; -import jdk.tools.jlink.internal.plugins.DefaultStripDebugPlugin; - +import jdk.tools.jlink.plugin.Plugin; +import jdk.tools.jlink.plugin.ResourcePool; +import jdk.tools.jlink.plugin.ResourcePoolBuilder; import tests.Helper; import tests.JImageGenerator; @@ -74,8 +71,6 @@ import tests.JImageGenerator; */ public class IntegrationTest { - private static final List ordered = new ArrayList<>(); - public static class MyPostProcessor implements PostProcessor, Plugin { public static final String NAME = "mypostprocessor"; @@ -162,7 +157,7 @@ public class IntegrationTest { limits.add("java.management"); JlinkConfiguration config = new Jlink.JlinkConfiguration(output, mods, - JlinkTask.newModuleFinder(modulePaths, limits, mods)); + JlinkTask.newModuleFinder(modulePaths, limits, mods), false, false, false); List lst = new ArrayList<>(); diff --git a/test/jdk/tools/jlink/JLinkDedupTestBatchSizeOne.java b/test/jdk/tools/jlink/JLinkDedupTestBatchSizeOne.java index 8b2dee1b45a..c7af8865a79 100644 --- a/test/jdk/tools/jlink/JLinkDedupTestBatchSizeOne.java +++ b/test/jdk/tools/jlink/JLinkDedupTestBatchSizeOne.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 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 @@ -21,14 +21,16 @@ * questions. */ -import jdk.test.lib.compiler.CompilerUtils; -import tests.JImageGenerator; - import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import jdk.test.lib.compiler.CompilerUtils; +import jdk.tools.jlink.internal.LinkableRuntimeImage; +import tests.JImageGenerator; + + /* * @test * @summary Make sure that modules can be linked using jlink @@ -54,10 +56,6 @@ public class JLinkDedupTestBatchSizeOne { private static final Path SRC_DIR = Paths.get(TEST_SRC, "dedup", "src"); private static final Path MODS_DIR = Paths.get("mods"); - private static final String MODULE_PATH = - Paths.get(JAVA_HOME, "jmods").toString() + - File.pathSeparator + MODS_DIR.toString(); - // the names of the modules in this test private static String[] modules = new String[]{"m1", "m2", "m3", "m4"}; @@ -69,8 +67,13 @@ public class JLinkDedupTestBatchSizeOne { return true; } - public static void compileAll() throws Throwable { - if (!hasJmods()) return; + private static String modulePath(boolean linkableRuntime) { + return (linkableRuntime ? "" : (Paths.get(JAVA_HOME, "jmods").toString() + + File.pathSeparator)) + MODS_DIR.toString(); + } + + public static void compileAll(boolean linkableRuntime) throws Throwable { + if (!linkableRuntime && !hasJmods()) return; for (String mn : modules) { Path msrc = SRC_DIR.resolve(mn); @@ -80,11 +83,15 @@ public class JLinkDedupTestBatchSizeOne { } public static void main(String[] args) throws Throwable { - compileAll(); + boolean linkableRuntime = LinkableRuntimeImage.isLinkableRuntime(); + System.out.println("Running test on " + + (linkableRuntime ? "enabled" : "disabled") + + " capability of linking from the run-time image."); + compileAll(linkableRuntime); Path image = Paths.get("bug8311591"); JImageGenerator.getJLinkTask() - .modulePath(MODULE_PATH) + .modulePath(modulePath(linkableRuntime)) .output(image.resolve("out-jlink-dedup")) .addMods("m1") .addMods("m2") diff --git a/test/jdk/tools/jlink/JLinkHelpCapabilityTest.java b/test/jdk/tools/jlink/JLinkHelpCapabilityTest.java new file mode 100644 index 00000000000..50f570251d2 --- /dev/null +++ b/test/jdk/tools/jlink/JLinkHelpCapabilityTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.spi.ToolProvider; + +import jdk.tools.jlink.internal.LinkableRuntimeImage; + +/* + * @test + * @summary Test jlink --help for capability output + * @modules jdk.jlink/jdk.tools.jlink.internal + * @requires vm.compMode != "Xcomp" + * @run main/othervm -Duser.language=en JLinkHelpCapabilityTest + */ +public class JLinkHelpCapabilityTest { + static final ToolProvider JLINK_TOOL = ToolProvider.findFirst("jlink") + .orElseThrow(() -> + new RuntimeException("jlink tool not found") + ); + + public static void main(String[] args) throws Exception { + boolean runtimeLinkCap = LinkableRuntimeImage.isLinkableRuntime(); + String capabilities = String.format("Linking from run-time image %s", + runtimeLinkCap ? "enabled" : "disabled"); + { + // Verify capability in --help output + StringWriter writer = new StringWriter(); + PrintWriter pw = new PrintWriter(writer); + JLINK_TOOL.run(pw, pw, "--help"); + String output = writer.toString().trim(); + String lines[] = output.split("\n"); + String capabilitiesMsg = null; + boolean seenCap = false; + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("Capabilities:")) { + seenCap = true; + continue; // skip 'Capabilities:' + } + if (!seenCap) { + continue; + } else { + // Line after capabilities is the message we care about + capabilitiesMsg = lines[i].trim(); + break; + } + } + System.out.println("DEBUG: Capabilities:"); + System.out.println("DEBUG: " + capabilitiesMsg); + if (!capabilities.equals(capabilitiesMsg)) { + System.err.println(output); + throw new AssertionError("'--help': Capabilities mismatch. Expected: '" + + capabilities +"' but got '" + capabilitiesMsg + "'"); + } + } + } +} diff --git a/test/jdk/tools/jlink/multireleasejar/JLinkMRJavaBaseVersionTest.java b/test/jdk/tools/jlink/multireleasejar/JLinkMRJavaBaseVersionTest.java index 46912a68033..1fba932798d 100644 --- a/test/jdk/tools/jlink/multireleasejar/JLinkMRJavaBaseVersionTest.java +++ b/test/jdk/tools/jlink/multireleasejar/JLinkMRJavaBaseVersionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 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 @@ -27,6 +27,7 @@ * @summary jlink should use the version from java.base.jmod to find modules * @bug 8185130 * @summary jlink should throw error if target image and current JDK versions don't match + * @requires jlink.packagedModules * @modules java.base/jdk.internal.module * @library /test/lib * @build jdk.test.lib.process.* CheckRuntimeVersion diff --git a/test/jdk/tools/jlink/plugins/GenerateJLIClassesPluginTest.java b/test/jdk/tools/jlink/plugins/GenerateJLIClassesPluginTest.java index 5d06288fb59..1acad93f5d3 100644 --- a/test/jdk/tools/jlink/plugins/GenerateJLIClassesPluginTest.java +++ b/test/jdk/tools/jlink/plugins/GenerateJLIClassesPluginTest.java @@ -21,6 +21,9 @@ * questions. */ +import static java.lang.constant.ConstantDescs.CD_Object; +import static java.lang.constant.ConstantDescs.CD_int; + import java.io.IOException; import java.lang.classfile.ClassFile; import java.lang.constant.MethodTypeDesc; @@ -32,16 +35,15 @@ import java.util.List; import java.util.stream.Collectors; import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import jdk.tools.jlink.internal.LinkableRuntimeImage; import tests.Helper; import tests.JImageGenerator; import tests.JImageValidator; import tests.Result; -import org.testng.annotations.BeforeTest; -import org.testng.annotations.Test; - -import static java.lang.constant.ConstantDescs.CD_Object; -import static java.lang.constant.ConstantDescs.CD_int; /* * @test @@ -63,12 +65,20 @@ public class GenerateJLIClassesPluginTest { @BeforeTest public static void setup() throws Exception { - helper = Helper.newHelper(); + boolean isLinkableRuntime = LinkableRuntimeImage.isLinkableRuntime(); + System.out.println("DEBUG: Tests run on " + + (isLinkableRuntime ? "enabled" : "disabled") + + " capability of linking from the run-time image."); + System.out.println("DEBUG: default module-path, 'jmods', " + + (Helper.jdkHasPackagedModules() ? "" : "NOT ") + + "present."); + helper = Helper.newHelper(isLinkableRuntime); if (helper == null) { + // In case of no linkable run-time image and also no packaged + // modules, helper will be null. System.err.println("Test not run"); return; } - helper.generateDefaultModules(); } @Test @@ -79,7 +89,6 @@ public class GenerateJLIClassesPluginTest { String fileString = "[SPECIES_RESOLVE] java.lang.invoke.BoundMethodHandle$Species_" + species + " (salvaged)\n"; Files.write(baseFile, fileString.getBytes(Charset.defaultCharset())); Result result = JImageGenerator.getJLinkTask() - .modulePath(helper.defaultModulePath()) .output(helper.createNewImageDir("generate-jli-file")) .option("--generate-jli-classes=@" + baseFile.toString()) .addMods("java.base") @@ -105,7 +114,6 @@ public class GenerateJLIClassesPluginTest { fileString = "[LF_RESOLVE] java.lang.invoke.DirectMethodHandle$Holder invokeVirtual L_L (success)\n"; Files.write(failFile, fileString.getBytes(Charset.defaultCharset())); Result result = JImageGenerator.getJLinkTask() - .modulePath(helper.defaultModulePath()) .output(helper.createNewImageDir("invalid-signature")) .option("--generate-jli-classes=@" + failFile.toString()) .addMods("java.base") @@ -118,7 +126,6 @@ public class GenerateJLIClassesPluginTest { @Test public static void nonExistentTraceFile() throws IOException { Result result = JImageGenerator.getJLinkTask() - .modulePath(helper.defaultModulePath()) .output(helper.createNewImageDir("non-existent-tracefile")) .option("--generate-jli-classes=@NON_EXISTENT_FILE") .addMods("java.base") @@ -134,7 +141,6 @@ public class GenerateJLIClassesPluginTest { Path invokersTrace = Files.createTempFile("invokers", "trace"); Files.writeString(invokersTrace, fileString, Charset.defaultCharset()); Result result = JImageGenerator.getJLinkTask() - .modulePath(helper.defaultModulePath()) .output(helper.createNewImageDir("jli-invokers")) .option("--generate-jli-classes=@" + invokersTrace.toString()) .addMods("java.base") @@ -183,4 +189,5 @@ public class GenerateJLIClassesPluginTest { .map(s -> "/java.base/java/lang/invoke/BoundMethodHandle$Species_" + s + ".class") .collect(Collectors.toList()); } + } diff --git a/test/jdk/tools/jlink/plugins/IncludeLocalesPluginTest.java b/test/jdk/tools/jlink/plugins/IncludeLocalesPluginTest.java index 2a0c43d3168..e2dd3403c83 100644 --- a/test/jdk/tools/jlink/plugins/IncludeLocalesPluginTest.java +++ b/test/jdk/tools/jlink/plugins/IncludeLocalesPluginTest.java @@ -22,25 +22,27 @@ */ import java.nio.file.Path; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; -import jdk.tools.jlink.plugin.PluginException; +import jdk.tools.jlink.internal.LinkableRuntimeImage; import jdk.tools.jlink.internal.TaskHelper; import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; +import jdk.tools.jlink.plugin.PluginException; import tests.Helper; import tests.JImageGenerator; import tests.JImageValidator; import tests.Result; + /* * @test * @bug 8152143 8152704 8155649 8165804 8185841 8176841 8190918 * 8179071 8202537 8221432 8222098 8251317 8258794 8265315 - * 8296248 8306116 8174269 8333582 + * 8296248 8306116 8174269 * @summary IncludeLocalesPlugin tests * @author Naoto Sato * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) @@ -59,14 +61,14 @@ import tests.Result; */ public class IncludeLocalesPluginTest { - private final static String moduleName = "IncludeLocalesTest"; + private static final String moduleName = "IncludeLocalesTest"; private static Helper helper; - private final static int INCLUDE_LOCALES_OPTION = 0; - private final static int ADDMODS_OPTION = 1; - private final static int EXPECTED_LOCATIONS = 2; - private final static int UNEXPECTED_PATHS = 3; - private final static int AVAILABLE_LOCALES = 4; - private final static int ERROR_MESSAGE = 5; + private static final int INCLUDE_LOCALES_OPTION = 0; + private static final int ADDMODS_OPTION = 1; + private static final int EXPECTED_LOCATIONS = 2; + private static final int UNEXPECTED_PATHS = 3; + private static final int AVAILABLE_LOCALES = 4; + private static final int ERROR_MESSAGE = 5; private static int errors; @@ -413,11 +415,18 @@ public class IncludeLocalesPluginTest { }; public static void main(String[] args) throws Exception { - helper = Helper.newHelper(); + boolean isLinkableRuntime = LinkableRuntimeImage.isLinkableRuntime(); + System.out.println("Running test on " + + (isLinkableRuntime ? "enabled" : "disabled") + + " capability of linking from the run-time image."); + System.out.println("Default module-path, 'jmods', " + + (Helper.jdkHasPackagedModules() ? "" : "NOT ") + + "present."); + + helper = Helper.newHelper(isLinkableRuntime); if (helper == null) { throw new RuntimeException("Helper could not be initialized"); } - helper.generateDefaultModules(); for (Object[] data : testData) { // create image for each test data @@ -425,14 +434,12 @@ public class IncludeLocalesPluginTest { if (data[INCLUDE_LOCALES_OPTION].toString().isEmpty()) { System.out.println("Invoking jlink with no --include-locales option"); result = JImageGenerator.getJLinkTask() - .modulePath(helper.defaultModulePath()) .output(helper.createNewImageDir(moduleName)) .addMods((String) data[ADDMODS_OPTION]) .call(); } else { System.out.println("Invoking jlink with \"" + data[INCLUDE_LOCALES_OPTION] + "\""); result = JImageGenerator.getJLinkTask() - .modulePath(helper.defaultModulePath()) .output(helper.createNewImageDir(moduleName)) .addMods((String) data[ADDMODS_OPTION]) .option((String) data[INCLUDE_LOCALES_OPTION]) diff --git a/test/jdk/tools/jlink/runtimeImage/AbstractLinkableRuntimeTest.java b/test/jdk/tools/jlink/runtimeImage/AbstractLinkableRuntimeTest.java new file mode 100644 index 00000000000..e7d5340e3b0 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/AbstractLinkableRuntimeTest.java @@ -0,0 +1,705 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Scanner; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; +import jdk.tools.jlink.internal.LinkableRuntimeImage; +import tests.Helper; +import tests.JImageGenerator; +import tests.JImageGenerator.JLinkTask; +import tests.JImageValidator; + +public abstract class AbstractLinkableRuntimeTest { + + protected static final boolean DEBUG = true; + + public void run() throws Exception { + boolean isLinkableRuntime = LinkableRuntimeImage.isLinkableRuntime(); + Helper helper = Helper.newHelper(isLinkableRuntime); + if (helper == null) { + System.err.println(AbstractLinkableRuntimeTest.class.getSimpleName() + + ": Test not run"); + return; + } + runTest(helper, isLinkableRuntime); + System.out.println(getClass().getSimpleName() + " PASSED!"); + } + + /** + * Main test entry point that actual tests ought to override. + * + * @param helper The jlink helper + * @param isLinkableRuntime {@code true} iff the JDK build under test already + * includes the linkable runtime capability in jlink. + * @throws Exception + */ + abstract void runTest(Helper helper, boolean isLinkableRuntime) throws Exception; + + /** + * Ensure 'java --list-modules' lists the correct set of modules in the given + * image. + * + * @param jlinkImage + * @param expectedModules + */ + protected void verifyListModules(Path image, + List expectedModules) throws Exception { + OutputAnalyzer out = runJavaCmd(image, List.of("--list-modules")); + List actual = parseListMods(out.getStdout()); + Collections.sort(actual); + if (!expectedModules.equals(actual)) { + throw new AssertionError("Different modules! Expected " + expectedModules + " got: " + actual); + } + } + + protected OutputAnalyzer runJavaCmd(Path image, List options) throws Exception { + Path targetJava = image.resolve("bin").resolve(getJava()); + List cmd = new ArrayList<>(); + cmd.add(targetJava.toString()); + for (String opt: options) { + cmd.add(opt); + } + List javaCmd = Collections.unmodifiableList(cmd); + OutputAnalyzer out; + try { + out = ProcessTools.executeCommand(javaCmd.toArray(new String[0])); + } catch (Throwable e) { + throw new Exception("Process failed to execute", e); + } + if (out.getExitValue() != 0) { + if (DEBUG) { + System.err.println("Process stdout was: "); + System.err.println(out.getStdout()); + System.err.println("Process stderr was: "); + System.err.println(out.getStderr()); + } + throw new AssertionError("'" + javaCmd.stream().collect(Collectors.joining(" ")) + "'" + + " expected to succeed!"); + } + return out; + } + + protected Path createJavaImageRuntimeLink(BaseJlinkSpec baseSpec) throws Exception { + return createJavaImageRuntimeLink(baseSpec, Collections.emptySet() /* exclude all jmods */); + } + + protected Path createJavaImageRuntimeLink(BaseJlinkSpec baseSpec, Set excludedJmods) throws Exception { + // Be sure we have a JDK without JMODs + Path runtimeJlinkImage = createRuntimeLinkImage(baseSpec, excludedJmods); + + // On Windows jvm.dll is in 'bin' after the jlink + Path libjvm = Path.of((isWindows() ? "bin" : "lib"), "server", System.mapLibraryName("jvm")); + JlinkSpecBuilder builder = new JlinkSpecBuilder(); + // And expect libjvm (not part of the jimage) to be present in the resulting image + builder.expectedFile(libjvm.toString()) + .helper(baseSpec.getHelper()) + .name(baseSpec.getName()) + .validatingModule(baseSpec.getValidatingModule()) + .imagePath(runtimeJlinkImage) + .expectedLocation("/java.base/java/lang/String.class"); + for (String m: baseSpec.getModules()) { + builder.addModule(m); + } + for (String extra: baseSpec.getExtraOptions()) { + builder.extraJlinkOpt(extra); + } + return jlinkUsingImage(builder.build()); + } + + protected Path jlinkUsingImage(JlinkSpec spec) throws Exception { + return jlinkUsingImage(spec, new RuntimeLinkOutputAnalyzerHandler()); + } + + protected Path jlinkUsingImage(JlinkSpec spec, OutputAnalyzerHandler handler) throws Exception { + return jlinkUsingImage(spec, handler, new DefaultSuccessExitPredicate()); + } + + protected Path jlinkUsingImage(JlinkSpec spec, OutputAnalyzerHandler handler, Predicate exitChecker) throws Exception { + String generatedImage = "target-run-time-" + spec.getName(); + Path targetImageDir = spec.getHelper().createNewImageDir(generatedImage); + Path targetJlink = spec.getImageToUse().resolve("bin").resolve(getJlink()); + String[] jlinkCmdArray = new String[] { + targetJlink.toString(), + "--output", targetImageDir.toString(), + "--verbose", + "--add-modules", spec.getModules().stream().collect(Collectors.joining(",")) + }; + List jlinkCmd = new ArrayList<>(); + jlinkCmd.addAll(Arrays.asList(jlinkCmdArray)); + if (spec.getExtraJlinkOpts() != null && !spec.getExtraJlinkOpts().isEmpty()) { + jlinkCmd.addAll(spec.getExtraJlinkOpts()); + } + if (spec.getModulePath() != null) { + for (String mp: spec.getModulePath()) { + jlinkCmd.add("--module-path"); + jlinkCmd.add(mp); + } + } + jlinkCmd = Collections.unmodifiableList(jlinkCmd); // freeze + System.out.println("DEBUG: run-time image based jlink command: " + + jlinkCmd.stream().collect(Collectors.joining(" "))); + OutputAnalyzer analyzer = null; + try { + analyzer = ProcessTools.executeProcess(jlinkCmd.toArray(new String[0])); + } catch (Throwable t) { + throw new AssertionError("Executing process failed!", t); + } + if (!exitChecker.test(analyzer)) { + if (DEBUG) { + System.err.println("Process stdout was: "); + System.err.println(analyzer.getStdout()); + System.err.println("Process stderr was: "); + System.err.println(analyzer.getStderr()); + } + // if the exit checker failed, we expected the other outcome + // i.e. fail for success and success for fail. + boolean successExit = analyzer.getExitValue() == 0; + String msg = String.format("Expected jlink to %s given a jmodless image. Exit code was: %d", + (successExit ? "fail" : "pass"), analyzer.getExitValue()); + throw new AssertionError(msg); + } + handler.handleAnalyzer(analyzer); // Give tests a chance to process in/output + + // validate the resulting image; Includes running 'java -version', only do this + // if the jlink succeeded. + if (analyzer.getExitValue() == 0) { + JImageValidator validator = new JImageValidator(spec.getValidatingModule(), spec.getExpectedLocations(), + targetImageDir.toFile(), spec.getUnexpectedLocations(), Collections.emptyList(), spec.getExpectedFiles()); + validator.validate(); // This doesn't validate locations + if (!spec.getExpectedLocations().isEmpty() || !spec.getUnexpectedLocations().isEmpty()) { + JImageValidator.validate(targetImageDir.resolve("lib").resolve("modules"), spec.getExpectedLocations(), spec.getUnexpectedLocations()); + } + } + return targetImageDir; + } + + /** + * Prepares the test for execution. This assumes the current runtime + * supports linking from it. However, since the 'jmods' dir might be present + * (default jmods module path), the 'jmods' directory needs to get removed + * to provoke actual linking from the run-time image. + * + * @param baseSpec + * @return A path to a JDK that is capable for linking from the run-time + * image. + * @throws Exception + */ + protected Path createRuntimeLinkImage(BaseJlinkSpec baseSpec) throws Exception { + return createRuntimeLinkImage(baseSpec, Collections.emptySet() /* exclude all jmods */); + } + + /** + * Prepares the test for execution. Creates a JDK with a jlink that has the + * capability to link from the run-time image (if needed). It further + * ensures that if packaged modules ('jmods' dir) are present, to remove + * them entirely or as specified in the {@link excludedJmodFiles} set. If + * that set is empty, all packaged modules will be removed. Note that with + * packaged modules present no run-time image based linking would be done. + * + * @param baseSpec + * The specification for the custom - run-time image link capable + * - JDK to create via jlink (if any) + * @param excludedJmods + * The set of jmod files to exclude in the base JDK. Empty set if + * all JMODs should be removed. + * @return A path to a JDK, including jdk.jlink, that has the run-time image + * link capability. + * + * @throws Exception + */ + protected Path createRuntimeLinkImage(BaseJlinkSpec baseSpec, + Set excludedJmodFiles) throws Exception { + // Depending on the shape of the JDK under test, we either only filter + // jmod files or create a run-time image link capable JDK on-the-fly. + Path from = null; + Path runtimeJlinkImage = null; + String finalName = baseSpec.getName() + "-jlink"; + if (baseSpec.isLinkableRuntime()) { + // The build is already run-time image link capable + String javaHome = System.getProperty("java.home"); + from = Path.of(javaHome); + } else { + // Create a run-time image capable JDK using --generate-linkable-runtime + Path tempRuntimeImage = Path.of(finalName + "-tmp"); + JLinkTask task = JImageGenerator.getJLinkTask(); + task.output(tempRuntimeImage) + .addMods("jdk.jlink") // jdk.jlink module is always needed for the test + .option("--generate-linkable-runtime"); + if (baseJDKhasPackagedModules()) { + Path jmodsPath = tempRuntimeImage.resolve("jmods"); + task.option("--keep-packaged-modules=" + jmodsPath); + } + for (String module: baseSpec.getModules()) { + task.addMods(module); + } + task.call().assertSuccess(); + from = tempRuntimeImage; + } + + // Create the target directory + runtimeJlinkImage = baseSpec.getHelper().createNewImageDir(finalName); + + // Remove JMODs as needed for the test + copyJDKTreeWithoutSpecificJmods(from, runtimeJlinkImage, excludedJmodFiles); + // Verify the base image is actually without desired packaged modules + if (excludedJmodFiles.isEmpty()) { + if (Files.exists(runtimeJlinkImage.resolve("jmods"))) { + throw new AssertionError("Must not contain 'jmods' directory"); + } + } else { + Path basePath = runtimeJlinkImage.resolve("jmods"); + for (String jmodFile: excludedJmodFiles) { + Path unexpectedFile = basePath.resolve(Path.of(jmodFile)); + if (Files.exists(unexpectedFile)) { + throw new AssertionError("Must not contain jmod: " + unexpectedFile); + } + } + } + return runtimeJlinkImage; + } + + private boolean baseJDKhasPackagedModules() { + Path jmodsPath = Path.of(System.getProperty("java.home"), "jmods"); + return jmodsPath.toFile().exists(); + } + + private void copyJDKTreeWithoutSpecificJmods(Path from, + Path to, + Set excludedJmods) throws Exception { + if (Files.exists(to)) { + throw new AssertionError("Expected target dir '" + to + "' to exist"); + } + FileVisitor fileVisitor = null; + if (excludedJmods.isEmpty()) { + fileVisitor = new ExcludeAllJmodsFileVisitor(from, to); + } else { + fileVisitor = new FileExcludingFileVisitor(excludedJmods, + from, + to); + } + Files.walkFileTree(from, fileVisitor); + } + + private List parseListMods(String output) throws Exception { + List outputLines = new ArrayList<>(); + try (Scanner lineScan = new Scanner(output)) { + while (lineScan.hasNextLine()) { + outputLines.add(lineScan.nextLine()); + } + } + return outputLines.stream() + .map(a -> { return a.split("@", 2)[0];}) + .filter(a -> !a.isBlank()) + .collect(Collectors.toList()); + } + + private String getJlink() { + return getBinary("jlink"); + } + + private String getJava() { + return getBinary("java"); + } + + private String getBinary(String binary) { + return isWindows() ? binary + ".exe" : binary; + } + + protected static boolean isWindows() { + return System.getProperty("os.name").startsWith("Windows"); + } + + static class ExcludeAllJmodsFileVisitor extends SimpleFileVisitor { + private final Path root; + private final Path destination; + + private ExcludeAllJmodsFileVisitor(Path root, + Path destination) { + this.destination = destination; + this.root = root; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, + BasicFileAttributes attrs) throws IOException { + Objects.requireNonNull(dir); + Path relative = root.relativize(dir); + if (relative.getFileName().equals(Path.of("jmods"))) { + return FileVisitResult.SKIP_SUBTREE; + } + // Create dir in destination location + Path targetDir = destination.resolve(relative); + if (!Files.exists(targetDir)) { + Files.createDirectory(targetDir); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Path relative = root.relativize(file); + Files.copy(file, destination.resolve(relative), StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + } + + static class FileExcludingFileVisitor extends SimpleFileVisitor { + + private final Set filesToExclude; + private final Path root; + private final Path destination; + + private FileExcludingFileVisitor(Set filesToExclude, + Path root, + Path destination) { + this.filesToExclude = filesToExclude; + this.destination = destination; + this.root = root; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, + BasicFileAttributes attrs) throws IOException { + Objects.requireNonNull(dir); + Path relative = root.relativize(dir); + // Create dir in destination location + Path targetDir = destination.resolve(relative); + if (!Files.exists(targetDir)) { + Files.createDirectory(targetDir); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Path relative = root.relativize(file); + // Skip files as determined by the exclude set + String fileName = file.getFileName().toString(); + if (!filesToExclude.contains(fileName)) { + Files.copy(file, destination.resolve(relative), StandardCopyOption.REPLACE_EXISTING); + } + return FileVisitResult.CONTINUE; + } + + } + + static class BaseJlinkSpec { + final Helper helper; + final String name; + final String validatingModule; + final List modules; + final List extraOptions; + final boolean isLinkableRuntime; + + BaseJlinkSpec(Helper helper, String name, String validatingModule, + List modules, List extraOptions, boolean isLinkableRuntime) { + this.helper = helper; + this.name = name; + this.modules = modules; + this.extraOptions = extraOptions; + this.validatingModule = validatingModule; + this.isLinkableRuntime = isLinkableRuntime; + } + + public String getValidatingModule() { + return validatingModule; + } + + public Helper getHelper() { + return helper; + } + + public String getName() { + return name; + } + + public List getModules() { + return modules; + } + + public List getExtraOptions() { + return extraOptions; + } + + public boolean isLinkableRuntime() { + return isLinkableRuntime; + } + } + + static class BaseJlinkSpecBuilder { + Helper helper; + String name; + String validatingModule; + List modules = new ArrayList<>(); + List extraOptions = new ArrayList<>(); + boolean isLinkableRuntime; + + BaseJlinkSpecBuilder addModule(String module) { + modules.add(module); + return this; + } + + BaseJlinkSpecBuilder addExtraOption(String option) { + extraOptions.add(option); + return this; + } + + BaseJlinkSpecBuilder setLinkableRuntime() { + isLinkableRuntime = true; + return this; + } + + BaseJlinkSpecBuilder helper(Helper helper) { + this.helper = helper; + return this; + } + + BaseJlinkSpecBuilder name(String name) { + this.name = name; + return this; + } + + BaseJlinkSpecBuilder validatingModule(String module) { + this.validatingModule = module; + return this; + } + + BaseJlinkSpec build() { + if (name == null) { + throw new IllegalStateException("Name must be set"); + } + if (helper == null) { + throw new IllegalStateException("helper must be set"); + } + if (modules.isEmpty()) { + throw new IllegalStateException("modules must be set"); + } + if (validatingModule == null) { + throw new IllegalStateException("the module which should get validated must be set"); + } + return new BaseJlinkSpec(helper, name, validatingModule, modules, extraOptions, isLinkableRuntime); + } + } + + static class JlinkSpec { + final Path imageToUse; + final Helper helper; + final String name; + final List modules; + final String validatingModule; + final List expectedLocations; + final List unexpectedLocations; + final String[] expectedFiles; + final List extraJlinkOpts; + final List modulePath; + + JlinkSpec(Path imageToUse, Helper helper, String name, List modules, + String validatingModule, List expectedLocations, + List unexpectedLocations, String[] expectedFiles, + List extraJlinkOpts, + List modulePath) { + this.imageToUse = imageToUse; + this.helper = helper; + this.name = name; + this.modules = modules; + this.validatingModule = validatingModule; + this.expectedLocations = expectedLocations; + this.unexpectedLocations = unexpectedLocations; + this.expectedFiles = expectedFiles; + this.extraJlinkOpts = extraJlinkOpts; + this.modulePath = modulePath; + } + + public Path getImageToUse() { + return imageToUse; + } + + public Helper getHelper() { + return helper; + } + + public String getName() { + return name; + } + + public List getModules() { + return modules; + } + + public String getValidatingModule() { + return validatingModule; + } + + public List getExpectedLocations() { + return expectedLocations; + } + + public List getUnexpectedLocations() { + return unexpectedLocations; + } + + public String[] getExpectedFiles() { + return expectedFiles; + } + + public List getExtraJlinkOpts() { + return extraJlinkOpts; + } + + public List getModulePath() { + return modulePath; + } + } + + static class JlinkSpecBuilder { + Path imageToUse; + Helper helper; + String name; + List modules = new ArrayList<>(); + String validatingModule; + List expectedLocations = new ArrayList<>(); + List unexpectedLocations = new ArrayList<>(); + List expectedFiles = new ArrayList<>(); + List extraJlinkOpts = new ArrayList<>(); + List modulePath = new ArrayList<>(); + + JlinkSpec build() { + if (imageToUse == null) { + throw new IllegalStateException("No image to use for jlink specified!"); + } + if (helper == null) { + throw new IllegalStateException("No helper specified!"); + } + if (name == null) { + throw new IllegalStateException("No name for the image location specified!"); + } + if (validatingModule == null) { + throw new IllegalStateException("No module specified for after generation validation!"); + } + return new JlinkSpec(imageToUse, + helper, + name, + modules, + validatingModule, + expectedLocations, + unexpectedLocations, + expectedFiles.toArray(new String[0]), + extraJlinkOpts, + modulePath); + } + + JlinkSpecBuilder imagePath(Path image) { + this.imageToUse = image; + return this; + } + + JlinkSpecBuilder helper(Helper helper) { + this.helper = helper; + return this; + } + + JlinkSpecBuilder name(String name) { + this.name = name; + return this; + } + + JlinkSpecBuilder addModule(String module) { + modules.add(module); + return this; + } + + JlinkSpecBuilder validatingModule(String module) { + this.validatingModule = module; + return this; + } + + JlinkSpecBuilder addModulePath(String modulePath) { + this.modulePath.add(modulePath); + return this; + } + + JlinkSpecBuilder expectedLocation(String location) { + expectedLocations.add(location); + return this; + } + + JlinkSpecBuilder unexpectedLocation(String location) { + unexpectedLocations.add(location); + return this; + } + + JlinkSpecBuilder expectedFile(String file) { + expectedFiles.add(file); + return this; + } + + JlinkSpecBuilder extraJlinkOpt(String opt) { + extraJlinkOpts.add(opt); + return this; + } + } + + static abstract class OutputAnalyzerHandler { + + public abstract void handleAnalyzer(OutputAnalyzer out); + + } + + static class RuntimeLinkOutputAnalyzerHandler extends OutputAnalyzerHandler { + + @Override + public void handleAnalyzer(OutputAnalyzer out) { + out.shouldContain("Linking based on the current run-time image"); + } + + } + + static class DefaultSuccessExitPredicate implements Predicate { + + @Override + public boolean test(OutputAnalyzer t) { + return t.getExitValue() == 0; + } + + } +} diff --git a/test/jdk/tools/jlink/runtimeImage/AddOptionsTest.java b/test/jdk/tools/jlink/runtimeImage/AddOptionsTest.java new file mode 100644 index 00000000000..1ffe1240d07 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/AddOptionsTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.List; +import java.util.Scanner; + +import jdk.test.lib.process.OutputAnalyzer; +import tests.Helper; + +/* + * @test + * @summary Test --add-options jlink plugin when linking from the run-time image + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g AddOptionsTest + */ +public class AddOptionsTest extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + AddOptionsTest test = new AddOptionsTest(); + test.run(); + } + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .addExtraOption("--add-options") + .addExtraOption("-Xlog:gc=info:stderr -XX:+UseParallelGC") + .name("java-base-with-opts") + .addModule("java.base") + .validatingModule("java.base") + .helper(helper); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + Path finalImage = createJavaImageRuntimeLink(builder.build()); + verifyListModules(finalImage, List.of("java.base")); + verifyParallelGCInUse(finalImage); + } + + private void verifyParallelGCInUse(Path finalImage) throws Exception { + OutputAnalyzer analyzer = runJavaCmd(finalImage, List.of("--version")); + boolean foundMatch = false; + try (Scanner lineScan = new Scanner(analyzer.getStderr())) { + while (lineScan.hasNextLine()) { + String line = lineScan.nextLine(); + if (line.endsWith("Using Parallel")) { + foundMatch = true; + break; + } + } + } + if (!foundMatch) { + throw new AssertionError("Expected Parallel GC in place for jlinked image"); + } + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/BasicJlinkMissingJavaBase.java b/test/jdk/tools/jlink/runtimeImage/BasicJlinkMissingJavaBase.java new file mode 100644 index 00000000000..b0d2a2d66f5 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/BasicJlinkMissingJavaBase.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +import tests.Helper; + + +/* + * @test + * @summary Test basic linking from the run-time image with java.base.jmod missing + * but java.xml.jmod present. It should link from the run-time image without errors. + * @requires (jlink.packagedModules & vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g BasicJlinkMissingJavaBase + */ +public class BasicJlinkMissingJavaBase extends AbstractLinkableRuntimeTest { + + @Override + public void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + Path finalImage = createJavaXMLRuntimeLink(helper, "java-xml", isLinkableRuntime); + verifyListModules(finalImage, List.of("java.base", "java.xml")); + } + + private Path createJavaXMLRuntimeLink(Helper helper, String name, boolean isLinkableRuntime) throws Exception { + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder(); + builder.helper(helper) + .name(name) + .addModule("java.xml") + .validatingModule("java.xml"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + Set excludedJmods = Set.of("java.base.jmod"); + return createJavaImageRuntimeLink(builder.build(), excludedJmods); + } + + public static void main(String[] args) throws Exception { + BasicJlinkMissingJavaBase test = new BasicJlinkMissingJavaBase(); + test.run(); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/BasicJlinkTest.java b/test/jdk/tools/jlink/runtimeImage/BasicJlinkTest.java new file mode 100644 index 00000000000..b97ebff9b49 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/BasicJlinkTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.List; + +import tests.Helper; + + +/* + * @test + * @summary Test basic linking from the run-time image + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g BasicJlinkTest false + */ +public class BasicJlinkTest extends AbstractLinkableRuntimeTest { + + @Override + public void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + Path finalImage = createJavaBaseRuntimeLink(helper, "java-base", isLinkableRuntime); + verifyListModules(finalImage, List.of("java.base")); + } + + private Path createJavaBaseRuntimeLink(Helper helper, String name, boolean isLinkableRuntime) throws Exception { + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder(); + builder.helper(helper) + .name(name) + .addModule("java.base") + .validatingModule("java.base"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + return createJavaImageRuntimeLink(builder.build()); + } + + public static void main(String[] args) throws Exception { + BasicJlinkTest test = new BasicJlinkTest(); + test.run(); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/CapturingHandler.java b/test/jdk/tools/jlink/runtimeImage/CapturingHandler.java new file mode 100644 index 00000000000..b6b6105394d --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/CapturingHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import jdk.test.lib.process.OutputAnalyzer; + +class CapturingHandler extends AbstractLinkableRuntimeTest.OutputAnalyzerHandler { + + private OutputAnalyzer output; + + public String stdErr() { + return output.getStderr(); + } + + public OutputAnalyzer analyzer() { + return output; + } + + @Override + public void handleAnalyzer(OutputAnalyzer out) { + this.output = out; + } +} \ No newline at end of file diff --git a/test/jdk/tools/jlink/runtimeImage/CustomModuleJlinkTest.java b/test/jdk/tools/jlink/runtimeImage/CustomModuleJlinkTest.java new file mode 100644 index 00000000000..369bccfecfc --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/CustomModuleJlinkTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.List; + +import tests.Helper; + + +/* + * @test + * @summary Test jmod-less jlink with a custom module + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g CustomModuleJlinkTest + */ +public class CustomModuleJlinkTest extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + CustomModuleJlinkTest test = new CustomModuleJlinkTest(); + test.run(); + } + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + String customModule = "leaf1"; + helper.generateDefaultJModule(customModule); + + // create a base image for linking from the run-time image + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .helper(helper) + .name("cmod-jlink") + .addModule("java.base") + .validatingModule("java.base"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + Path jlinkImage = createRuntimeLinkImage(builder.build()); + + // Next jlink using the run-time image for java.base, but take + // the custom module from the module path. + Path finalImage = jlinkUsingImage(new JlinkSpecBuilder() + .imagePath(jlinkImage) + .helper(helper) + .name(customModule) + .addModulePath(helper.defaultModulePath(false)) + .expectedLocation(String.format("/%s/%s/com/foo/bar/X.class", customModule, customModule)) + .addModule(customModule) + .validatingModule(customModule) + .build()); + // Expected only the transitive closure of "leaf1" module in the --list-modules + // output of the java launcher. + List expectedModules = List.of("java.base", customModule); + verifyListModules(finalImage, expectedModules); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/GenerateJLIClassesTest.java b/test/jdk/tools/jlink/runtimeImage/GenerateJLIClassesTest.java new file mode 100644 index 00000000000..533a8db30d0 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/GenerateJLIClassesTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import tests.Helper; + +/* + * @test + * @summary Verify JLI class generation in run-time image link mode + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g GenerateJLIClassesTest + */ +public class GenerateJLIClassesTest extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + GenerateJLIClassesTest test = new GenerateJLIClassesTest(); + test.run(); + } + + /* + * java.lang.invoke.BoundMethodHandle$Species_* classes get generated + * by the GenerateJLiClassesPlugin. This test ensures that potentially + * generated JLI classes from the run-time image don't populate to the + * target image in the run-time image based link mode. + */ + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + Path baseFile = Files.createTempFile("base", "trace"); + String species = "LLLLLLLLLLLLLLLLLLL"; + String fileString = "[SPECIES_RESOLVE] java.lang.invoke.BoundMethodHandle$Species_" + species + " (salvaged)\n"; + Files.write(baseFile, fileString.getBytes(StandardCharsets.UTF_8)); + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .helper(helper) + .addModule("java.base") + .name("jlink.jli-jmodless") + .validatingModule("java.base"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + + Path runtimeLinkableImage = createRuntimeLinkImage(builder.build()); + // Finally attempt another jmodless link reducing modules to java.base only, + // and asking for specific jli classes. + jlinkUsingImage(new JlinkSpecBuilder() + .helper(helper) + .imagePath(runtimeLinkableImage) + .name("java.base-jli-derived") + .addModule("java.base") + .extraJlinkOpt("--generate-jli-classes=@" + baseFile.toString()) + .expectedLocation("/java.base/java/lang/invoke/BoundMethodHandle$Species_" + species + ".class") + .expectedLocation("/java.base/java/lang/invoke/BoundMethodHandle$Species_L.class") + .unexpectedLocation("/java.base/java/lang/invoke/BoundMethodHandle$Species_" + species.substring(1) + ".class") + .unexpectedLocation("/java.base/java/lang/invoke/BoundMethodHandle$Species_LL.class") + .validatingModule("java.base") + .build()); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/JImageHelper.java b/test/jdk/tools/jlink/runtimeImage/JImageHelper.java new file mode 100644 index 00000000000..ba92e1f35f2 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/JImageHelper.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023, Red Hat, Inc. + * 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. + * + * 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. + */ + + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jdk.internal.jimage.BasicImageReader; +import jdk.internal.jimage.ImageLocation; + +/** + * + * JDK Modular image iterator + */ +public class JImageHelper { + + private JImageHelper() { + // Don't instantiate + } + + public static List listContents(Path jimage) throws IOException { + try(BasicImageReader reader = BasicImageReader.open(jimage)) { + List entries = new ArrayList<>(); + for (String s : reader.getEntryNames()) { + entries.add(s); + } + Collections.sort(entries); + return entries; + } + } + + public static byte[] getLocationBytes(String location, Path jimage) throws IOException { + try(BasicImageReader reader = BasicImageReader.open(jimage)) { + ImageLocation il = reader.findLocation(location); + byte[] r = reader.getResource(il); + if (r == null) { + throw new IllegalStateException(String.format("bytes for %s not found!", location)); + } + return r; + } + } +} diff --git a/test/jdk/tools/jlink/runtimeImage/JavaSEReproducibleTest.java b/test/jdk/tools/jlink/runtimeImage/JavaSEReproducibleTest.java new file mode 100644 index 00000000000..d923358aed9 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/JavaSEReproducibleTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Files; +import java.nio.file.Path; + +import tests.Helper; + + +/* + * @test + * @summary Test reproducibility of linking an java.se image using the run-time + * image. + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g JavaSEReproducibleTest + */ +public class JavaSEReproducibleTest extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + JavaSEReproducibleTest test = new JavaSEReproducibleTest(); + test.run(); + } + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + String javaSeModule = "java.se"; + // create a java.se using jmod-less approach + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .helper(helper) + .addModule(javaSeModule) + .validatingModule(javaSeModule); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + builder.name("java-se-repro1"); + Path javaSEJmodLess1 = createJavaImageRuntimeLink(builder.build()); + + // create another java.se version using jmod-less approach + builder.name("java-se-repro2"); + Path javaSEJmodLess2 = createJavaImageRuntimeLink(builder.build()); + if (Files.mismatch(javaSEJmodLess1.resolve("lib").resolve("modules"), + javaSEJmodLess2.resolve("lib").resolve("modules")) != -1L) { + throw new RuntimeException("jlink producing inconsistent result for " + javaSeModule + " (jmod-less)"); + } + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/KeepPackagedModulesFailTest.java b/test/jdk/tools/jlink/runtimeImage/KeepPackagedModulesFailTest.java new file mode 100644 index 00000000000..8094579ecd5 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/KeepPackagedModulesFailTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.function.Predicate; + +import jdk.test.lib.process.OutputAnalyzer; +import tests.Helper; + + +/* + * @test + * @summary Verify that jlink with an empty module path, but trying to use + * --keep-packaged-modules fails as expected. + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g KeepPackagedModulesFailTest + */ +public class KeepPackagedModulesFailTest extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + KeepPackagedModulesFailTest test = new KeepPackagedModulesFailTest(); + test.run(); + } + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + // create a base image for linking from the run-time image + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .helper(helper) + .name("jlink-fail") + .addModule("java.base") + .validatingModule("java.base"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + Path baseImage = createRuntimeLinkImage(builder.build()); + + CapturingHandler handler = new CapturingHandler(); + Predicate exitFailPred = new Predicate<>() { + + @Override + public boolean test(OutputAnalyzer t) { + return t.getExitValue() != 0; // expect failure + } + }; + // Attempt a jlink using the run-time image and also using option + // --keep-packaged-modules, which should fail. + jlinkUsingImage(new JlinkSpecBuilder() + .helper(helper) + .imagePath(baseImage) + .name("java-base-jlink-keep-packaged-target") + .addModule("java.base") + .extraJlinkOpt("--keep-packaged-modules=foo") + .validatingModule("java.base") + .build(), handler, exitFailPred); + OutputAnalyzer analyzer = handler.analyzer(); + if (analyzer.getExitValue() == 0) { + throw new AssertionError("Expected jlink to have failed!"); + } + analyzer.stdoutShouldContain("Error"); + analyzer.stdoutShouldContain("--keep-packaged-modules"); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/ModifiedFilesExitTest.java b/test/jdk/tools/jlink/runtimeImage/ModifiedFilesExitTest.java new file mode 100644 index 00000000000..709494b6256 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/ModifiedFilesExitTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.function.Predicate; + +import jdk.test.lib.process.OutputAnalyzer; +import tests.Helper; + +/* + * @test + * @summary Verify jlink fails by default when linking from the run-time image + * and files have been modified + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g ModifiedFilesExitTest + */ +public class ModifiedFilesExitTest extends ModifiedFilesTest { + + public static void main(String[] args) throws Exception { + ModifiedFilesExitTest test = new ModifiedFilesExitTest(); + test.run(); + } + + @Override + String initialImageName() { + return "java-base-jlink-with-mod-exit"; + } + + @Override + void testAndAssert(Path modifiedFile, Helper helper, Path initialImage) + throws Exception { + CapturingHandler handler = new CapturingHandler(); + Predicate exitFailPred = new Predicate<>() { + + @Override + public boolean test(OutputAnalyzer t) { + return t.getExitValue() != 0; // expect failure + } + }; + jlinkUsingImage(new JlinkSpecBuilder() + .helper(helper) + .imagePath(initialImage) + .name("java-base-jlink-with-mod-exit-target") + .addModule("java.base") + .validatingModule("java.base") + .build(), handler, exitFailPred); + OutputAnalyzer analyzer = handler.analyzer(); + if (analyzer.getExitValue() == 0) { + throw new AssertionError("Expected jlink to fail due to modified file!"); + } + analyzer.stdoutShouldContain(modifiedFile.toString() + " has been modified"); + // Verify the error message is reasonable + analyzer.stdoutShouldNotContain("jdk.tools.jlink.internal.RunImageLinkException"); + analyzer.stdoutShouldNotContain("java.lang.IllegalArgumentException"); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/ModifiedFilesTest.java b/test/jdk/tools/jlink/runtimeImage/ModifiedFilesTest.java new file mode 100644 index 00000000000..305fdd39171 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/ModifiedFilesTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +import tests.Helper; + +public abstract class ModifiedFilesTest extends AbstractLinkableRuntimeTest { + + abstract String initialImageName(); + abstract void testAndAssert(Path modifiedFile, Helper helper, Path initialImage) throws Exception; + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .name(initialImageName()) + .addModule("java.base") + .validatingModule("java.base") + .helper(helper); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + Path initialImage = createRuntimeLinkImage(builder.build()); + + Path netPropertiesFile = modifyFileInImage(initialImage); + + testAndAssert(netPropertiesFile, helper, initialImage); + } + + protected Path modifyFileInImage(Path jmodLessImg) + throws IOException, AssertionError { + // modify net.properties config file + Path netPropertiesFile = jmodLessImg.resolve("conf").resolve("net.properties"); + Properties props = new Properties(); + try (InputStream is = Files.newInputStream(netPropertiesFile)) { + props.load(is); + } + String prevVal = (String)props.put("java.net.useSystemProxies", Boolean.TRUE.toString()); + if (prevVal == null || Boolean.getBoolean(prevVal) != false) { + throw new AssertionError("Expected previous value to be false!"); + } + try (OutputStream out = Files.newOutputStream(netPropertiesFile)) { + props.store(out, "Modified net.properties file!"); + } + return netPropertiesFile; + } +} diff --git a/test/jdk/tools/jlink/runtimeImage/ModifiedFilesWarningTest.java b/test/jdk/tools/jlink/runtimeImage/ModifiedFilesWarningTest.java new file mode 100644 index 00000000000..f52691dd859 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/ModifiedFilesWarningTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; + +import jdk.test.lib.process.OutputAnalyzer; +import tests.Helper; + +/* + * @test + * @summary Verify warnings are being produced when linking from the run-time + * image and files have been modified + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g ModifiedFilesWarningTest + */ +public class ModifiedFilesWarningTest extends ModifiedFilesTest { + + protected static final String IGNORE_MODIFIED_RUNTIME_OPT = "--ignore-modified-runtime"; + + public static void main(String[] args) throws Exception { + ModifiedFilesWarningTest test = new ModifiedFilesWarningTest(); + test.run(); + } + + @Override + String initialImageName() { + return "java-base-jlink-with-mod-warn"; + } + + @Override + void testAndAssert(Path modifiedFile, Helper helper, Path initialImage) throws Exception { + CapturingHandler handler = new CapturingHandler(); + jlinkUsingImage(new JlinkSpecBuilder() + .helper(helper) + .imagePath(initialImage) + .name("java-base-jlink-with-mod-warn-target") + .addModule("java.base") + .validatingModule("java.base") + .extraJlinkOpt(IGNORE_MODIFIED_RUNTIME_OPT) // only generate a warning + .build(), handler); + OutputAnalyzer out = handler.analyzer(); + // verify we get the warning message + out.stdoutShouldMatch("Warning: .* has been modified"); + out.stdoutShouldNotContain("java.lang.IllegalArgumentException"); + out.stdoutShouldNotContain("jdk.tools.jlink.internal.RunImageLinkException"); + } +} diff --git a/test/jdk/tools/jlink/runtimeImage/MultiHopTest.java b/test/jdk/tools/jlink/runtimeImage/MultiHopTest.java new file mode 100644 index 00000000000..88f91f238bd --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/MultiHopTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.function.Predicate; + +import jdk.test.lib.process.OutputAnalyzer; +import tests.Helper; + + +/* + * @test + * @summary Verify that a jlink using the run-time image cannot include jdk.jlink + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g MultiHopTest + */ +public class MultiHopTest extends AbstractLinkableRuntimeTest { + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + Path jdkJlinkJmodless = createJDKJlinkJmodLess(helper, "jdk.jlink-multi-hop1", isLinkableRuntime); + CapturingHandler handler = new CapturingHandler(); + Predicate exitFailPred = new Predicate<>() { + + @Override + public boolean test(OutputAnalyzer a) { + return a.getExitValue() != 0; // expect failure + } + }; + jlinkUsingImage(new JlinkSpecBuilder() + .helper(helper) + .imagePath(jdkJlinkJmodless) + .name("jdk-jlink-multi-hop1-target") + .addModule("jdk.jlink") + .validatingModule("java.base") + .build(), handler, exitFailPred); + OutputAnalyzer analyzer = handler.analyzer(); + if (analyzer.getExitValue() == 0) { + throw new AssertionError("Expected jlink to fail due to including jdk.jlink"); + } + String expectedMsg = "This JDK does not contain packaged modules " + + "and cannot be used to create another image with " + + "the jdk.jlink module"; + analyzer.stdoutShouldContain(expectedMsg); + analyzer.stdoutShouldNotContain("Exception"); // ensure error message is sane + } + + private Path createJDKJlinkJmodLess(Helper helper, String name, boolean isLinkableRuntime) throws Exception { + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder(); + builder.helper(helper) + .name(name) + .addModule("jdk.jlink") + .validatingModule("java.base"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + return createRuntimeLinkImage(builder.build()); + } + + public static void main(String[] args) throws Exception { + MultiHopTest test = new MultiHopTest(); + test.run(); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java b/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java new file mode 100644 index 00000000000..9910be5f919 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import tests.Helper; +import tests.JImageGenerator; + + +/* + * @test + * @summary Compare packaged-modules jlink with a run-time image based jlink to + * produce the same result + * @requires (jlink.packagedModules & vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g PackagedModulesVsRuntimeImageLinkTest + */ +public class PackagedModulesVsRuntimeImageLinkTest extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + PackagedModulesVsRuntimeImageLinkTest test = new PackagedModulesVsRuntimeImageLinkTest(); + test.run(); + } + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + // create a java.se using jmod-less approach + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .helper(helper) + .name("java-se-jmodless") + .addModule("java.se") + .validatingModule("java.se"); + if (isLinkableRuntime) { + 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")) + .addMods("java.se").call().assertSuccess(); + + compareRecursively(javaSEruntimeLink, javaSEJmodFull); + } + + // Visit all files in the given directories checking that they're byte-by-byte identical + private static void compareRecursively(Path javaSEJmodLess, + Path javaSEJmodFull) throws IOException, AssertionError { + FilesCapturingVisitor jmodFullVisitor = new FilesCapturingVisitor(javaSEJmodFull); + FilesCapturingVisitor jmodLessVisitor = new FilesCapturingVisitor(javaSEJmodLess); + Files.walkFileTree(javaSEJmodFull, jmodFullVisitor); + Files.walkFileTree(javaSEJmodLess, jmodLessVisitor); + List jmodFullFiles = jmodFullVisitor.filesVisited(); + List jmodLessFiles = jmodLessVisitor.filesVisited(); + Collections.sort(jmodFullFiles); + Collections.sort(jmodLessFiles); + + if (jmodFullFiles.size() != jmodLessFiles.size()) { + throw new AssertionError(String.format("Size of files different for jmod-less (%d) vs jmod-full (%d) java.se jlink", jmodLessFiles.size(), jmodFullFiles.size())); + } + String jimageFile = Path.of("lib").resolve("modules").toString(); + // Compare all files except the modules image + for (int i = 0; i < jmodFullFiles.size(); i++) { + String jmodFullPath = jmodFullFiles.get(i); + String jmodLessPath = jmodLessFiles.get(i); + if (!jmodFullPath.equals(jmodLessPath)) { + throw new AssertionError(String.format("jmod-full path (%s) != jmod-less path (%s)", jmodFullPath, jmodLessPath)); + } + if (jmodFullPath.equals(jimageFile)) { + continue; + } + Path a = javaSEJmodFull.resolve(Path.of(jmodFullPath)); + Path b = javaSEJmodLess.resolve(Path.of(jmodLessPath)); + if (Files.mismatch(a, b) != -1L) { + handleFileMismatch(a, b); + } + } + // Compare jimage contents by iterating its entries and comparing their + // paths and content bytes + // + // Note: The files aren't byte-by-byte comparable (probably due to string hashing + // and offset differences in container bytes) + Path jimageJmodLess = javaSEJmodLess.resolve(Path.of("lib")).resolve(Path.of("modules")); + 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())); + } + 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'", + i, + jimageContentJmodFull.get(i), + jimageContentJmodLess.get(i) + )); + } + String loc = jimageContentJmodFull.get(i); + if (isTreeInfoResource(loc)) { + // Skip container bytes as those are offsets to the content + // of the container which might be different between jlink runs. + continue; + } + byte[] resBytesFull = JImageHelper.getLocationBytes(loc, jimageJmodFull); + byte[] resBytesLess = JImageHelper.getLocationBytes(loc, jimageJmodLess); + if (resBytesFull.length != resBytesLess.length || Arrays.mismatch(resBytesFull, resBytesLess) != -1) { + throw new AssertionError("Content bytes mismatch for " + loc); + } + } + } + + private static boolean isTreeInfoResource(String path) { + return path.startsWith("/packages") || path.startsWith("/modules"); + } + + private static void handleFileMismatch(Path a, Path b) { + throw new AssertionError("Files mismatch: " + a + " vs. " + b); + } + + static class FilesCapturingVisitor extends SimpleFileVisitor { + private final Path basePath; + private final List filePaths = new ArrayList<>(); + public FilesCapturingVisitor(Path basePath) { + this.basePath = basePath; + } + + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) { + Path relative = basePath.relativize(path); + filePaths.add(relative.toString()); + return FileVisitResult.CONTINUE; + } + + List filesVisited() { + return filePaths; + } + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/PatchedJDKModuleJlinkTest.java b/test/jdk/tools/jlink/runtimeImage/PatchedJDKModuleJlinkTest.java new file mode 100644 index 00000000000..3baa824e049 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/PatchedJDKModuleJlinkTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Predicate; +import java.util.spi.ToolProvider; + +import jdk.test.lib.process.OutputAnalyzer; +import tests.Helper; + + +/* + * @test + * @summary Test run-time link with --patch-module. Expect failure. + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g PatchedJDKModuleJlinkTest + */ +public class PatchedJDKModuleJlinkTest extends AbstractLinkableRuntimeTest { + + @Override + public void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + String imageName = "java-base-patched"; + Path runtimeLinkImage = createRuntimeLinkImage(helper, imageName + "-base", isLinkableRuntime); + + // Prepare patched module content + Path patchSource = Path.of("java-base-patch-src"); + Path pkg = patchSource.resolve("java", "lang"); + Path extraClass = pkg.resolve("MyJlinkPatchInteger.java"); + String source = """ + package java.lang; + public class MyJlinkPatchInteger { + public int add(int a, int b) { + return a + b; + } + } + """; + Files.createDirectories(pkg); + Files.writeString(extraClass, source); + Path patchClasses = Path.of("java-base-patch-classes"); + Files.createDirectories(patchClasses); + ToolProvider javac = ToolProvider.findFirst("javac") + .orElseThrow(() -> new AssertionError("javac not found")); + javac.run(System.out, System.err, new String[] { + "-d", patchClasses.toString(), + "--patch-module=java.base=" + patchSource.toAbsolutePath().toString(), + extraClass.toAbsolutePath().toString() + }); + + // Perform a run-time image link expecting a failure + CapturingHandler handler = new CapturingHandler(); + Predicate exitFailPred = new Predicate<>() { + + @Override + public boolean test(OutputAnalyzer t) { + return t.getExitValue() != 0; // expect failure + } + }; + jlinkUsingImage(new JlinkSpecBuilder() + .helper(helper) + .imagePath(runtimeLinkImage) + .name(imageName + "-derived") + .addModule("java.base") + .validatingModule("java.base") + .extraJlinkOpt("-J--patch-module=java.base=" + + patchClasses.toAbsolutePath().toString()) + .build(), handler, exitFailPred); + OutputAnalyzer analyzer = handler.analyzer(); + if (analyzer.getExitValue() == 0) { + throw new AssertionError("Expected jlink to fail due to patched module!"); + } + analyzer.stdoutShouldContain("MyJlinkPatchInteger.class not found in the modules image."); + analyzer.stdoutShouldContain("--patch-module is not supported"); + // Verify the error message is reasonable + analyzer.stdoutShouldNotContain("jdk.tools.jlink.internal.RunImageLinkException"); + analyzer.stdoutShouldNotContain("java.lang.IllegalArgumentException"); + } + + private Path createRuntimeLinkImage(Helper helper, String name, boolean isLinkableRuntime) throws Exception { + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .name(name) + .addModule("java.base") + .validatingModule("java.base") + .helper(helper); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + return createRuntimeLinkImage(builder.build()); + } + + public static void main(String[] args) throws Exception { + PatchedJDKModuleJlinkTest test = new PatchedJDKModuleJlinkTest(); + test.run(); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/SystemModulesTest.java b/test/jdk/tools/jlink/runtimeImage/SystemModulesTest.java new file mode 100644 index 00000000000..fac8cac112d --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/SystemModulesTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import tests.Helper; +import tests.JImageValidator; + + +/* + * @test + * @summary Test appropriate handling of generated SystemModules* classes in run-time image link mode + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g SystemModulesTest + */ +public class SystemModulesTest extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + SystemModulesTest test = new SystemModulesTest(); + test.run(); + } + + /* + * SystemModule classes are module specific. If the jlink is based on the + * modules image, then earlier generated SystemModule classes shall not get + * propagated. + */ + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + // create an image with a module containing a main entrypoint (jdk.httpserver), + // thus producing the SystemModules$0.class. Add jdk.jdwp.agent as a module which + // isn't resolved by default, so as to generate SystemModules$default.class + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .helper(helper) + .name("httpserver-jlink-jmodless-derived") + .addModule("jdk.httpserver") + .addModule("jdk.jdwp.agent") + .validatingModule("java.base"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + Path javaseJmodless = createJavaImageRuntimeLink(builder.build()); + // Verify that SystemModules$0.class etc. are there, due to httpserver and jdwp.agent + JImageValidator.validate(javaseJmodless.resolve("lib").resolve("modules"), + List.of("/java.base/jdk/internal/module/SystemModules$default.class", + "/java.base/jdk/internal/module/SystemModules$0.class", + "/java.base/jdk/internal/module/SystemModules$all.class"), + Collections.emptyList()); + } + +} diff --git a/test/jdk/tools/jlink/runtimeImage/SystemModulesTest2.java b/test/jdk/tools/jlink/runtimeImage/SystemModulesTest2.java new file mode 100644 index 00000000000..6be4ad7321c --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/SystemModulesTest2.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024, Red Hat, Inc. + * 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. + * + * 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. + */ + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import tests.Helper; +import tests.JImageValidator; + + +/* + * @test + * @summary Test disabled SystemModulesPlugin in run-time image link mode. Expect + * generated classes to not be there. + * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @library ../../lib /test/lib + * @enablePreview + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jlink.internal + * jdk.jlink/jdk.tools.jlink.plugin + * jdk.jlink/jdk.tools.jimage + * @build tests.* jdk.test.lib.process.OutputAnalyzer + * jdk.test.lib.process.ProcessTools + * @run main/othervm -Xmx1g SystemModulesTest2 + */ +public class SystemModulesTest2 extends AbstractLinkableRuntimeTest { + + public static void main(String[] args) throws Exception { + SystemModulesTest2 test = new SystemModulesTest2(); + test.run(); + } + + @Override + void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { + // See SystemModulesTest which enables the system-modules plugin. With + // it disabled, we expect for the generated classes to not be there. + BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder() + .helper(helper) + .name("jlink-jmodless-sysmod2") + .addModule("jdk.httpserver") + .validatingModule("java.base") + .addExtraOption("--disable-plugin") + .addExtraOption("system-modules"); + if (isLinkableRuntime) { + builder.setLinkableRuntime(); + } + Path runtimeImageLinkTarget = createJavaImageRuntimeLink(builder.build()); + JImageValidator.validate(runtimeImageLinkTarget.resolve("lib").resolve("modules"), + Collections.emptyList(), + List.of("/java.base/jdk/internal/module/SystemModules$all.class", + "/java.base/jdk/internal/module/SystemModules$default.class", + "/java.base/jdk/internal/module/SystemModules$0.class")); + } + +} diff --git a/test/jdk/tools/lib/tests/Helper.java b/test/jdk/tools/lib/tests/Helper.java index 0acf6ec8bf3..337d767b0b2 100644 --- a/test/jdk/tools/lib/tests/Helper.java +++ b/test/jdk/tools/lib/tests/Helper.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 @@ -64,20 +64,30 @@ public class Helper { private final Map> moduleDependencies = new HashMap<>(); private final List bootClasses; private final FileSystem fs; + private final boolean linkableRuntime; + private static final Path JDK_HOME = Paths.get(System.getProperty("test.jdk")); public static Helper newHelper() throws IOException { - Path jdkHome = Paths.get(System.getProperty("test.jdk")); - if (!Files.exists(jdkHome.resolve("jmods"))) { + return newHelper(false); + } + + public static Helper newHelper(boolean linkableRuntime) throws IOException { + if (!linkableRuntime && !jdkHasPackagedModules()) { // Skip test if the jmods directory is missing (e.g. exploded image) System.err.println("Test not run, NO jmods directory"); return null; } - return new Helper(jdkHome); + return new Helper(JDK_HOME, linkableRuntime); } - private Helper(Path jdkHome) throws IOException { + public static boolean jdkHasPackagedModules() { + return Files.exists(JDK_HOME.resolve("jmods")); + } + + private Helper(Path jdkHome, boolean linkableRuntime) throws IOException { + this.linkableRuntime = linkableRuntime; this.stdjmods = jdkHome.resolve("jmods").normalize(); - if (!Files.exists(stdjmods)) { + if (!linkableRuntime && !Files.exists(stdjmods)) { throw new IOException("Standard jMods do not exist."); } this.fs = FileSystems.getFileSystem(URI.create("jrt:/")); @@ -140,7 +150,8 @@ public class Helper { } public String defaultModulePath(boolean includeStdMods) { - return (includeStdMods? stdjmods.toAbsolutePath().toString() : "") + File.pathSeparator + String standardMods = linkableRuntime ? "" : stdjmods.toAbsolutePath().toString() + File.pathSeparator; + return (includeStdMods? standardMods : "") + jmods.toAbsolutePath().toString() + File.pathSeparator + jars.toAbsolutePath().toString() + File.pathSeparator + explodedmodsclasses.toAbsolutePath().toString(); @@ -184,7 +195,7 @@ public class Helper { generateGarbage(jmodsclasses.resolve(moduleName)); Path jmodFile = jmods.resolve(moduleName + ".jmod"); - JModTask task = JImageGenerator.getJModTask() + JModTask task = JImageGenerator.getJModTask(linkableRuntime) .jmod(jmodFile) .addJmods(stdjmods) .addJmods(jmods.toAbsolutePath()) diff --git a/test/jdk/tools/lib/tests/JImageGenerator.java b/test/jdk/tools/lib/tests/JImageGenerator.java index 330547148d0..b872ca4f584 100644 --- a/test/jdk/tools/lib/tests/JImageGenerator.java +++ b/test/jdk/tools/lib/tests/JImageGenerator.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 @@ -159,7 +159,11 @@ public class JImageGenerator { } public static JModTask getJModTask() { - return new JModTask(); + return getJModTask(false); + } + + public static JModTask getJModTask(boolean linkableRuntime) { + return new JModTask(linkableRuntime); } public static JLinkTask getJLinkTask() { @@ -350,11 +354,16 @@ public class JImageGenerator { private final List jars = new ArrayList<>(); private final List jmods = new ArrayList<>(); private final List options = new ArrayList<>(); + private final boolean linkableRuntime; private Path output; private String hashModules; private String mainClass; private String moduleVersion; + private JModTask(boolean linkableRuntime) { + this.linkableRuntime = linkableRuntime; + } + public JModTask addNativeLibraries(Path cp) { this.libs.add(cp); return this; @@ -414,7 +423,7 @@ public class JImageGenerator { // This is expect FIRST jmods THEN jars, if you change this, some tests could fail String jmods = toPath(this.jmods); String jars = toPath(this.jars); - return jmods + File.pathSeparator + jars; + return linkableRuntime ? jars : jmods + File.pathSeparator + jars; } private String toPath(List paths) { diff --git a/test/jtreg-ext/requires/VMProps.java b/test/jtreg-ext/requires/VMProps.java index 4f00846116c..775b95959c6 100644 --- a/test/jtreg-ext/requires/VMProps.java +++ b/test/jtreg-ext/requires/VMProps.java @@ -138,6 +138,7 @@ public class VMProps implements Callable> { map.put("jdk.containerized", this::jdkContainerized); map.put("vm.flagless", this::isFlagless); map.put("jdk.foreign.linker", this::jdkForeignLinker); + map.put("jlink.packagedModules", this::packagedModules); vmGC(map); // vm.gc.X = true/false vmGCforCDS(map); // may set vm.gc vmOptFinalFlags(map); @@ -715,6 +716,21 @@ public class VMProps implements Callable> { return "" + "true".equalsIgnoreCase(isEnabled); } + private String packagedModules() { + // Some jlink tests require packaged modules being present (jmods). + // For a runtime linkable image build packaged modules aren't present + try { + Path jmodsDir = Path.of(System.getProperty("java.home"), "jmods"); + if (jmodsDir.toFile().exists()) { + return Boolean.TRUE.toString(); + } else { + return Boolean.FALSE.toString(); + } + } catch (Throwable t) { + return Boolean.FALSE.toString(); + } + } + /** * Checks if we are in almost out-of-box configuration, i.e. the flags * which JVM is started with don't affect its behavior "significantly". diff --git a/test/langtools/tools/javac/plugin/AutostartPlugins.java b/test/langtools/tools/javac/plugin/AutostartPlugins.java index eeac5e0ecc6..5eb5b16bad5 100644 --- a/test/langtools/tools/javac/plugin/AutostartPlugins.java +++ b/test/langtools/tools/javac/plugin/AutostartPlugins.java @@ -30,7 +30,7 @@ * jdk.compiler/com.sun.tools.javac.main * jdk.jlink * @build toolbox.ToolBox toolbox.JavacTask toolbox.JarTask - * @run main AutostartPlugins + * @run main/othervm AutostartPlugins */ import java.io.IOException; diff --git a/test/langtools/tools/javac/plugin/InternalAPI.java b/test/langtools/tools/javac/plugin/InternalAPI.java index 523c9365c99..70bb024ede9 100644 --- a/test/langtools/tools/javac/plugin/InternalAPI.java +++ b/test/langtools/tools/javac/plugin/InternalAPI.java @@ -31,7 +31,7 @@ * jdk.compiler/com.sun.tools.javac.main * jdk.jlink * @build toolbox.ToolBox toolbox.JavacTask toolbox.JarTask - * @run main InternalAPI + * @run main/othervm InternalAPI */ import java.io.IOException;