diff --git a/make/CompileToolsJdk.gmk b/make/CompileToolsJdk.gmk index 13101c7cccf07..483367ace11a0 100644 --- a/make/CompileToolsJdk.gmk +++ b/make/CompileToolsJdk.gmk @@ -59,7 +59,10 @@ $(eval $(call SetupJavaCompilation, BUILD_TOOLS_JDK, \ --add-exports java.base/sun.text=ALL-UNNAMED \ --add-exports java.base/sun.security.util=ALL-UNNAMED \ --add-exports jdk.internal.opt/jdk.internal.opt=jdk.compiler.interim \ - --add-exports jdk.internal.opt/jdk.internal.opt=jdk.javadoc.interim, \ + --add-exports jdk.internal.opt/jdk.internal.opt=jdk.javadoc.interim \ + --add-exports java.base/jdk.internal.module=ALL-UNNAMED \ + --add-exports java.base/jdk.internal.jimage=ALL-UNNAMED \ + , \ )) TARGETS += $(BUILD_TOOLS_JDK) diff --git a/make/Images.gmk b/make/Images.gmk index adf53a83c4f4c..d7d0980471db0 100644 --- a/make/Images.gmk +++ b/make/Images.gmk @@ -30,6 +30,7 @@ include MakeBase.gmk include Execute.gmk include Modules.gmk include Utils.gmk +include ToolsJdk.gmk JDK_TARGETS := JRE_TARGETS := @@ -85,10 +86,22 @@ JLINK_TOOL := $(JLINK) -J-Djlink.debug=true \ JLINK_JRE_EXTRA_OPTS := --no-man-pages --no-header-files --strip-debug +ifeq ($(JLINK_PRODUCE_RUNTIME_LINK_JDK), true) + JDK_LINK_OUTPUT_DIR := $(RL_INTERMEDIATE_IMAGE_DIR) +else + JDK_LINK_OUTPUT_DIR := $(JDK_IMAGE_DIR) +endif + ifeq ($(JLINK_KEEP_PACKAGED_MODULES), true) - JLINK_JDK_EXTRA_OPTS := --keep-packaged-modules $(JDK_IMAGE_DIR)/jmods + ifeq ($(JLINK_PRODUCE_RUNTIME_LINK_JDK), true) + JLINK_JDK_EXTRA_OPTS := --keep-packaged-modules $(RL_INTERMEDIATE_IMAGE_DIR)/jmods + JLINK_RUNTIME_CREATE_EXTRA := --keep-packaged-modules $(JDK_IMAGE_DIR)/jmods + else + JLINK_JDK_EXTRA_OPTS := --keep-packaged-modules $(JDK_IMAGE_DIR)/jmods + endif endif + JLINK_DISABLE_WARNINGS := | ( $(GREP) -v -e "WARNING: Using incubator module" || test "$$?" = "1" ) JDK_IMAGE_SUPPORT_DIR := $(SUPPORT_OUTPUTDIR)/images/jdk @@ -98,16 +111,48 @@ $(eval $(call SetupExecute, jlink_jdk, \ WARN := Creating jdk image, \ DEPS := $(JDK_JMODS) $(BASE_RELEASE_FILE) \ $(call DependOnVariable, JDK_MODULES_LIST, $(JDK_IMAGE_SUPPORT_DIR)/_jlink_jdk.vardeps), \ - OUTPUT_DIR := $(JDK_IMAGE_DIR), \ + OUTPUT_DIR := $(JDK_LINK_OUTPUT_DIR), \ SUPPORT_DIR := $(JDK_IMAGE_SUPPORT_DIR), \ - PRE_COMMAND := $(RM) -r $(JDK_IMAGE_DIR), \ + PRE_COMMAND := $(RM) -r $(JDK_LINK_OUTPUT_DIR), \ COMMAND := $(JLINK_TOOL) --add-modules $(JDK_MODULES_LIST) \ - $(JLINK_JDK_EXTRA_OPTS) --output $(JDK_IMAGE_DIR) \ + $(JLINK_JDK_EXTRA_OPTS) --output $(JDK_LINK_OUTPUT_DIR) \ $(JLINK_DISABLE_WARNINGS), \ )) JLINK_JDK_TARGETS := $(jlink_jdk) +ifeq ($(JLINK_PRODUCE_RUNTIME_LINK_JDK), true) + + JDK_RUN_TIME_IMAGE_SUPPORT_DIR := $(SUPPORT_OUTPUTDIR)/images/jlink-run-time + JDK_JMODS_DIFF := $(JDK_RUN_TIME_IMAGE_SUPPORT_DIR)/jimage_packaged_modules_diff.data + JLINK_RUNTIME_CREATE_EXTRA += --create-linkable-runtime=$(JDK_JMODS_DIFF) + + $(eval $(call SetupExecute, generate_jimage_diff, \ + WARN := Generating the jimage to packaged modules diff, \ + DEPS := $(jlink_jdk), \ + OUTPUT_FILE := $(JDK_JMODS_DIFF), \ + SUPPORT_DIR := $(JDK_RUN_TIME_IMAGE_SUPPORT_DIR), \ + COMMAND := $(TOOL_JIMAGE_DIFF_TO_JMODS) $(IMAGES_OUTPUTDIR)/jmods \ + $(JDK_LINK_OUTPUT_DIR)/lib/modules $(JDK_JMODS_DIFF), \ + )) + + $(eval $(call SetupExecute, jlink_runtime_jdk, \ + WARN := Creating jdk image for runtime linking, \ + DEPS := $(generate_jimage_diff), \ + OUTPUT_DIR := $(JDK_IMAGE_DIR), \ + SUPPORT_DIR := $(JDK_RUN_TIME_IMAGE_SUPPORT_DIR), \ + PRE_COMMAND := $(RM) -r $(JDK_IMAGE_DIR), \ + COMMAND := $(JLINK_TOOL) --add-modules $(JDK_MODULES_LIST) \ + $(JLINK_RUNTIME_CREATE_EXTRA) \ + --output $(JDK_IMAGE_DIR) \ + $(JLINK_DISABLE_WARNINGS), \ + )) + + JLINK_JDK_TARGETS += $(generate_jimage_diff) + JLINK_JDK_TARGETS += $(jlink_runtime_jdk) + +endif + $(eval $(call SetupExecute, jlink_jre, \ WARN := Creating legacy jre image, \ DEPS := $(JRE_JMODS) $(BASE_RELEASE_FILE) \ diff --git a/make/ToolsJdk.gmk b/make/ToolsJdk.gmk index e7cc3fc871128..fc07fc122d514 100644 --- a/make/ToolsJdk.gmk +++ b/make/ToolsJdk.gmk @@ -82,6 +82,11 @@ TOOL_GENERATEEXTRAPROPERTIES = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_too TOOL_MAKEZIPREPRODUCIBLE = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ build.tools.makezipreproducible.MakeZipReproducible +TOOL_JIMAGE_DIFF_TO_JMODS = $(BUILD_JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes \ + --add-modules=jdk.jlink --add-exports=java.base/jdk.internal.module=ALL-UNNAMED \ + --add-exports=java.base/jdk.internal.jimage=ALL-UNNAMED \ + build.tools.runtimelink.JimageDiffGenerator + # TODO: There are references to the jdwpgen.jar in jdk/make/netbeans/jdwpgen/build.xml # and nbproject/project.properties in the same dir. Needs to be looked at. TOOL_JDWPGEN = $(JAVA_SMALL) -cp $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes build.tools.jdwpgen.Main diff --git a/make/autoconf/configure.ac b/make/autoconf/configure.ac index 6afa36ac18d33..e29d3618282e6 100644 --- a/make/autoconf/configure.ac +++ b/make/autoconf/configure.ac @@ -137,6 +137,7 @@ BASIC_SETUP_DEFAULT_LOG # We need build & target for this. JDKOPT_SETUP_JMOD_OPTIONS JDKOPT_SETUP_JLINK_OPTIONS +JDKOPT_SETUP_JLINK_RUN_TIME_LINK_IMAGE JDKVER_SETUP_JDK_VERSION_NUMBERS ############################################################################### diff --git a/make/autoconf/jdk-options.m4 b/make/autoconf/jdk-options.m4 index f56081223a600..544bff6cbfbfb 100644 --- a/make/autoconf/jdk-options.m4 +++ b/make/autoconf/jdk-options.m4 @@ -591,6 +591,22 @@ AC_DEFUN_ONCE([JDKOPT_SETUP_JLINK_OPTIONS], AC_SUBST(JLINK_KEEP_PACKAGED_MODULES) ]) +################################################################################ +# +# jlink related. +# +# Determines whether or not a run-time linkable JDK image is being +# produced from the product image. +# +AC_DEFUN_ONCE([JDKOPT_SETUP_JLINK_RUN_TIME_LINK_IMAGE], +[ + UTIL_ARG_ENABLE(NAME: runtime-link-image, DEFAULT: false, + RESULT: JLINK_PRODUCE_RUNTIME_LINK_JDK, + DESC: [enable producing an image suitable for runtime linking], + CHECKING_MSG: [whether or not an image suitable for runtime linking should be produced]) + AC_SUBST(JLINK_PRODUCE_RUNTIME_LINK_JDK) +]) + ################################################################################ # # Enable or disable generation of the classlist at build time diff --git a/make/autoconf/spec.gmk.template b/make/autoconf/spec.gmk.template index e9d53fcd77a56..c4acca941549d 100644 --- a/make/autoconf/spec.gmk.template +++ b/make/autoconf/spec.gmk.template @@ -731,6 +731,7 @@ NEW_JAVADOC = $(INTERIM_LANGTOOLS_ARGS) $(JAVADOC_MAIN_CLASS) JMOD_COMPRESS := @JMOD_COMPRESS@ JLINK_KEEP_PACKAGED_MODULES := @JLINK_KEEP_PACKAGED_MODULES@ +JLINK_PRODUCE_RUNTIME_LINK_JDK := @JLINK_PRODUCE_RUNTIME_LINK_JDK@ RCFLAGS := @RCFLAGS@ @@ -899,6 +900,10 @@ STATIC_LIBS_GRAAL_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(STATIC_LIBS_GRAAL_IMAGE_SUB GRAAL_BUILDER_IMAGE_SUBDIR := graal-builder-jdk GRAAL_BUILDER_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(GRAAL_BUILDER_IMAGE_SUBDIR) +# Runtime jdk link image +RL_INTERMEDIATE_IMAGE_SUBDIR := runtime-link-initial-jdk +RL_INTERMEDIATE_IMAGE_DIR := $(IMAGES_OUTPUTDIR)/$(RL_INTERMEDIATE_IMAGE_SUBDIR) + # Macosx bundles directory definitions JDK_MACOSX_BUNDLE_SUBDIR := jdk-bundle JRE_MACOSX_BUNDLE_SUBDIR := jre-bundle diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkCLIArgsListener.java b/make/jdk/src/classes/build/tools/runtimelink/ImageReader.java similarity index 50% rename from src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkCLIArgsListener.java rename to make/jdk/src/classes/build/tools/runtimelink/ImageReader.java index d95e9aa973c12..634a1767e6c42 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkCLIArgsListener.java +++ b/make/jdk/src/classes/build/tools/runtimelink/ImageReader.java @@ -1,12 +1,10 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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. + * 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 @@ -22,18 +20,37 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ +package build.tools.runtimelink; -package jdk.tools.jlink.internal; - +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; -/** - * - * Plugins wishing to observe the command list that was used to - * trigger the link must implement this interface. - * - */ -public interface JlinkCLIArgsListener { +import jdk.internal.jimage.BasicImageReader; + +public class ImageReader extends BasicImageReader implements JimageDiffGenerator.ImageResource { + + public ImageReader(Path path) throws IOException { + super(path); + } + + public static boolean isNotTreeInfoResource(String path) { + return !(path.startsWith("/packages") || path.startsWith("/modules")); + } + + @Override + public List getEntries() { + return Arrays.asList(getEntryNames()).stream() + .filter(ImageReader::isNotTreeInfoResource) + .sorted() + .collect(Collectors.toList()); + } + + @Override + public byte[] getResourceBytes(String name) { + return getResource(name); + } - public void process(List cliArgs); } diff --git a/make/jdk/src/classes/build/tools/runtimelink/JimageDiffGenerator.java b/make/jdk/src/classes/build/tools/runtimelink/JimageDiffGenerator.java new file mode 100644 index 0000000000000..75fe0824e574e --- /dev/null +++ b/make/jdk/src/classes/build/tools/runtimelink/JimageDiffGenerator.java @@ -0,0 +1,130 @@ +/* + * 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 build.tools.runtimelink; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class JimageDiffGenerator { + + private static final boolean DEBUG = false; + + @SuppressWarnings("try") + public interface ImageResource extends AutoCloseable { + public List getEntries(); + public byte[] getResourceBytes(String name); + } + + public List generateDiff(ImageResource baseImg, ImageResource optImage) throws Exception { + List baseResources; + Set optResSet = new HashSet<>(); + List diffs = new ArrayList<>(); + try (baseImg; + optImage) { + optResSet.addAll(optImage.getEntries()); + baseResources = baseImg.getEntries(); + for (String item: baseResources) { + byte[] baseBytes = baseImg.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 (!optResSet.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, optImage.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 optResSet are the resources only present in the + // optimized image (generated by some plugins; not present in jmods) + for (String e: optResSet) { + ResourceDiff.Builder builder = new ResourceDiff.Builder(); + ResourceDiff diff = builder.setKind(ResourceDiff.Kind.ADDED) + .setName(e) + .build(); + diffs.add(diff); + } + return diffs; + } + + public static void main(String[] args) throws Exception { + if (args.length != 3) { + System.out.println("Usage: java -cp jrt-fs.jar:. JimageDiffGenerator "); + System.exit(1); + } + ImageResource base = new JmodsReader(Path.of(args[0])); + ImageResource opt = new ImageReader(Path.of(args[1])); + JimageDiffGenerator diffGen = new JimageDiffGenerator(); + List diffs = diffGen.generateDiff(base, opt); + + if (DEBUG) { + printDiffs(diffs); + } + FileOutputStream fout = new FileOutputStream(new File(args[2])); + ResourceDiff.write(diffs, fout); + } + + private static void printDiffs(List diffs) { + for (ResourceDiff diff: diffs.stream().sorted().collect(Collectors.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/make/jdk/src/classes/build/tools/runtimelink/JmodsReader.java b/make/jdk/src/classes/build/tools/runtimelink/JmodsReader.java new file mode 100644 index 0000000000000..cc5c4744802ea --- /dev/null +++ b/make/jdk/src/classes/build/tools/runtimelink/JmodsReader.java @@ -0,0 +1,98 @@ +/* + * 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 build.tools.runtimelink; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReader; +import java.lang.module.ModuleReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import jdk.internal.module.ModulePath; + +@SuppressWarnings("try") +public class JmodsReader implements JimageDiffGenerator.ImageResource { + + private final ModuleFinder finder; + + public JmodsReader(Path packagedModulesDir) { + List pa; + try { + pa = Files.list(packagedModulesDir).filter(p -> !p.endsWith(".jmod")).collect(Collectors.toList()); + } catch (IOException e) { + throw new IllegalStateException("Listing of jmods directory failed!", e); + } + Path[] paths = pa.toArray(new Path[0]); + this.finder = ModulePath.of(Runtime.version(), true, paths); + } + + @Override + public void close() throws Exception { + // nothing + } + + @Override + public List getEntries() { + List all = new ArrayList<>(); + try { + Set allMods = finder.findAll(); + for (ModuleReference mRef: allMods) { + String moduleName = mRef.descriptor().name(); + ModuleReader reader = mRef.open(); + List perModule = reader.list().map(a -> {return "/" + moduleName + "/" + a;}).collect(Collectors.toList()); + all.addAll(perModule); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + Collections.sort(all); + return all; + } + + @Override + public byte[] getResourceBytes(String name) { + int secondSlash = name.indexOf("/", 1); + String moduleName = null; + if (secondSlash != -1) { + moduleName = name.substring(1, secondSlash); + } + if (moduleName == null) { + throw new IllegalArgumentException("Module name not found in " + name); + } + ModuleReference ref = finder.find(moduleName).orElseThrow(); + String refName = name.substring(secondSlash + 1); // omit the leading slash + try (ModuleReader reader = ref.open()) { + return reader.open(refName).orElseThrow().readAllBytes(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + +} diff --git a/make/jdk/src/classes/build/tools/runtimelink/ResourceDiff.java b/make/jdk/src/classes/build/tools/runtimelink/ResourceDiff.java new file mode 100644 index 0000000000000..d738e5e9f5519 --- /dev/null +++ b/make/jdk/src/classes/build/tools/runtimelink/ResourceDiff.java @@ -0,0 +1,272 @@ +/* + * 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 build.tools.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.List; +import java.util.Objects; + +/** + * Class representing a difference between 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. 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). + * + * Duplicated in jdk.jlink/jdk.tools.jlink.internal.ResourceDiff + */ +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 IllegalStateException("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 IllegalStateException("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 IllegalStateException("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); + } + } + } catch (IOException e) { + e.printStackTrace(); + throw e; + } + } + + /** + * 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; + 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(); + diffs = new ArrayList<>(numItems); + for (int i = 0; i < numItems; i++) { + Kind k = Kind.fromShort(din.readShort()); + int numBytes = din.readInt(); + byte[] buf = readNumBytesFromStream(din, numBytes); + String name = new String(buf, StandardCharsets.UTF_8); + numBytes = din.readInt(); + byte[] resBytes = null; + if (numBytes != 0) { + resBytes = readNumBytesFromStream(din, numBytes); + } + Builder builder = new Builder(); + builder.setKind(k) + .setName(name); + if (resBytes != null) { + builder.setResourceBytes(resBytes); + } + diffs.add(builder.build()); + } + } + return diffs; + } + + private static byte[] readNumBytesFromStream(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; + } +} diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginConfiguration.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginConfiguration.java index b32b4c0b36c6f..68b32db0e7f58 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginConfiguration.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginConfiguration.java @@ -63,15 +63,10 @@ public final class ImagePluginConfiguration { private ImagePluginConfiguration() { } - public static ImagePluginStack parseConfiguration(Jlink.PluginsConfiguration pluginsConfiguration) - throws Exception { - return parseConfiguration(pluginsConfiguration, null); - } - /* * Create a stack of plugins from a configuration. */ - public static ImagePluginStack parseConfiguration(Jlink.PluginsConfiguration pluginsConfiguration, List cli) + public static ImagePluginStack parseConfiguration(Jlink.PluginsConfiguration pluginsConfiguration) throws Exception { if (pluginsConfiguration == null) { return new ImagePluginStack(); @@ -135,6 +130,6 @@ public void storeFiles(ResourcePool files) { }; } - return new ImagePluginStack(builder, orderedPlugins, lastSorter, cli); + return new ImagePluginStack(builder, orderedPlugins, lastSorter); } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginStack.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginStack.java index 08491effb66bc..44dc9e69dd517 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginStack.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImagePluginStack.java @@ -28,16 +28,7 @@ import java.io.IOException; import java.lang.module.ModuleDescriptor; import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Stream; import jdk.internal.jimage.decompressor.Decompressor; @@ -171,25 +162,22 @@ public String getString(int id) { private final Plugin lastSorter; private final List plugins = new ArrayList<>(); private final List resourcePrevisitors = new ArrayList<>(); - private final List cliArgsListeners = new ArrayList<>(); private final boolean validate; - private final List cliArgs; public ImagePluginStack() { - this(null, Collections.emptyList(), null, null); + this(null, Collections.emptyList(), null); } public ImagePluginStack(ImageBuilder imageBuilder, List plugins, - Plugin lastSorter, List cliArgs) { - this(imageBuilder, plugins, lastSorter, true, cliArgs); + Plugin lastSorter) { + this(imageBuilder, plugins, lastSorter, true); } public ImagePluginStack(ImageBuilder imageBuilder, List plugins, Plugin lastSorter, - boolean validate, - List cliArgs) { + boolean validate) { this.imageBuilder = Objects.requireNonNull(imageBuilder); this.lastSorter = lastSorter; this.plugins.addAll(Objects.requireNonNull(plugins)); @@ -198,11 +186,7 @@ public ImagePluginStack(ImageBuilder imageBuilder, if (p instanceof ResourcePrevisitor) { resourcePrevisitors.add((ResourcePrevisitor) p); } - if (p instanceof JlinkCLIArgsListener) { - cliArgsListeners.add((JlinkCLIArgsListener) p); - } }); - this.cliArgs = cliArgs; this.validate = validate; } @@ -242,9 +226,6 @@ public ResourcePool visitResources(ResourcePoolManager resources) return new ResourcePoolManager(resources.byteOrder(), resources.getStringTable()).resourcePool(); } - cliArgsListeners.forEach((p) -> { - p.process(cliArgs); - }); PreVisitStrings previsit = new PreVisitStrings(); resourcePrevisitors.forEach((p) -> { p.previsit(resources.resourcePool(), previsit); diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java index 6c67656baf748..6d90eb3efcec6 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java @@ -25,8 +25,8 @@ package jdk.tools.jlink.internal; +import static jdk.tools.jlink.internal.JlinkTask.DIFF_PATTERN; import static jdk.tools.jlink.internal.JlinkTask.RESPATH_PATTERN; -import static jdk.tools.jlink.internal.JlinkTask.RUNIMAGE_LINK_STAMP; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -42,8 +42,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HexFormat; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -65,6 +67,7 @@ public class JRTArchive implements Archive { private final ModuleReference ref; private final List files = new ArrayList<>(); private final List otherRes; + private final Map resDiff; private final boolean errorOnModifiedFile; JRTArchive(String module, Path path, boolean errorOnModifiedFile) { @@ -76,6 +79,7 @@ public class JRTArchive implements Archive { new IllegalArgumentException("Module " + module + " not part of the JDK install")); this.errorOnModifiedFile = errorOnModifiedFile; this.otherRes = readModuleResourceFile(module); + this.resDiff = readModuleResourceDiff(module); } @Override @@ -134,26 +138,48 @@ public boolean equals(Object obj) { private void collectFiles() throws IOException { if (files.isEmpty()) { addNonClassResources(); - // Add classes/resources from image module + // Add classes/resources from image module, + // patched with the runtime image link diff files.addAll(ref.open().list() - .filter(s -> !RUNIMAGE_LINK_STAMP.equals(s)) + .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. + return (rd == null || rd.getKind() == ResourceDiff.Kind.MODIFIED); + }) .map(s -> { - return new JRTArchiveFile(JRTArchive.this, s, - EntryType.CLASS_OR_RESOURCE, null /* hashOrTarget */, false /* symlink */); - }).collect(Collectors.toList())); - - if (module.equals("jdk.jlink")) { - // this entry represents that the image being created is based on the - // run-time image (not the packaged modules). - files.add(createRuntimeImageLinkStamp()); - } + 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)); + }) + .collect(Collectors.toList())); + // Finally add all files only present in the resource diff + // That is, removed items in the runtime image. + files.addAll(resDiff.values().stream() + .filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED) + .map(s -> { + int secondSlash = s.getName().indexOf("/", 1); + if (secondSlash == -1) { + throw new AssertionError(); + } + String pathWithoutModule = s.getName().substring(secondSlash + 1, s.getName().length()); + return new JRTArchiveFile(JRTArchive.this, pathWithoutModule, + EntryType.CLASS_OR_RESOURCE, + null /* hashOrTarget */, + false /* symlink */, + s); + }) + .collect(Collectors.toList())); } } - private JRTFile createRuntimeImageLinkStamp() { - return new JRTArchiveStampFile(this, RUNIMAGE_LINK_STAMP, EntryType.CLASS_OR_RESOURCE, null, false); - } - /* * no need to keep track of the warning produced since this is eagerly checked once. */ @@ -179,7 +205,7 @@ private void addNonClassResources() { } } - return new JRTArchiveFile(JRTArchive.this, m.resPath, toEntryType(m.resType), m.hashOrTarget, m.symlink); + return new JRTArchiveFile(JRTArchive.this, m.resPath, toEntryType(m.resType), m.hashOrTarget, m.symlink, null); }) .collect(Collectors.toList())); } @@ -338,11 +364,11 @@ interface JRTFile { Entry toEntry(); } - record JRTArchiveFile (Archive archive, String resPath, EntryType resType, String sha, boolean symlink) + record JRTArchiveFile (Archive archive, String resPath, EntryType resType, String sha, boolean symlink, ResourceDiff diff) implements JRTFile { public Entry toEntry() { - return new Entry(archive, resPath, resPath, resType) { + return new Entry(archive, String.format("/%s/%s", archive.moduleName(), resPath), resPath, resType) { @Override public long size() { try { @@ -357,7 +383,15 @@ public long size() { // 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 + // this size query. + if (diff != null) { + // If the resource has a diff to the + // packaged modules, use the diff. Diffs of kind + // ADDED have been filtered in collectFiles(); + assert diff.getKind() != ResourceDiff.Kind.ADDED; + assert diff.getName().equals(String.format("/%s/%s", archive.moduleName(), resPath)); + return diff.getResourceBytes().length; + } return Files.size(archive.getPath().resolve(resPath)); } } catch (IOException e) { @@ -372,7 +406,13 @@ public InputStream stream() throws IOException { Path path = symlink ? BASE.resolve(sha) : BASE.resolve(resPath); return Files.newInputStream(path); } else { - // Read from the module image. + // Read from the module image. Use the diff to the + // packaged modules if we have one. + 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(); @@ -383,30 +423,6 @@ public InputStream stream() throws IOException { } } - // Stamp file marker for single-hop implementation - record JRTArchiveStampFile(Archive archive, String resPath, EntryType resType, String sha, boolean symlink) - implements JRTFile - { - @Override - public Entry toEntry() { - return new Entry(archive, resPath, resPath, resType) { - - @Override - public long size() { - // empty file - return 0; - } - - @Override - public InputStream stream() throws IOException { - // empty content - return new ByteArrayInputStream(new byte[0]); - } - - }; - } - } - static List readModuleResourceFile(String modName) { String resName = String.format(RESPATH_PATTERN, modName); try { @@ -423,4 +439,18 @@ static List readModuleResourceFile(String modName) { throw new InternalError("Failed to process run-time image resources for " + modName); } } + + static Map readModuleResourceDiff(String modName) { + String resName = String.format(DIFF_PATTERN, modName); + try { + try (InputStream inStream = JRTArchive.class.getModule().getResourceAsStream(resName)) { + List diffs = ResourceDiff.read(inStream); + Map resDiffsAsMap = new HashMap<>(); + diffs.forEach(r -> resDiffsAsMap.put(r.getName(), r)); + return resDiffsAsMap; + } + } catch (IOException e) { + throw new InternalError("Failed to process run-time image diff file for " + modName); + } + } } 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 72b95471d183c..3100744d2c0dc 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 @@ -28,7 +28,6 @@ import java.io.BufferedInputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -74,8 +73,6 @@ 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.plugins.LegalNoticeFilePlugin; -import jdk.tools.jlink.plugin.Plugin; import jdk.tools.jlink.plugin.PluginException; /** @@ -91,17 +88,7 @@ public class JlinkTask { private static final TaskHelper taskHelper = new TaskHelper(JLINK_BUNDLE); - - // Flag for scratch task validation in run-time image mode - private final boolean isScratch; - - public JlinkTask() { - this(false); - } - - private JlinkTask(boolean isScratch) { - this.isScratch = isScratch; - } + private static final String JLINK_MOD_NAME = "jdk.jlink"; private static final Option[] recognizedOptions = { new Option(false, (task, opt, arg) -> { @@ -185,7 +172,7 @@ private JlinkTask(boolean isScratch) { }, "--version"), new Option(true, (task, opt, arg) -> { Path path = Paths.get(arg); - if (!task.isScratch && Files.exists(path)) { + if (Files.exists(path)) { throw taskHelper.newBadArgs("err.dir.exists", path); } task.options.packagedModulesPath = path; @@ -201,7 +188,7 @@ private JlinkTask(boolean isScratch) { }, "--ignore-signing-information"), new Option(false, (task, opt, arg) -> { task.options.ignoreModifiedRuntime = true; - }, true, "--ignore-modified-runtime"), + }, true, "--ignore-modified-runtime") }; @@ -248,9 +235,8 @@ static class OptionsValues { public static final String OPTIONS_RESOURCE = "jdk/tools/jlink/internal/options"; public static final String RESPATH_PATTERN = "jdk/tools/jlink/internal/fs_%s_files"; - public static final String CLI_RESOURCE_FILE = "jdk/tools/jlink/internal/cli_cmd.txt"; - public static final String RUNIMAGE_LINK_STAMP = "jdk/tools/jlink/internal/runtimeimage.link.stamp"; - + // The diff file per module + public static final String DIFF_PATTERN = "jdk/tools/jlink/internal/diff_%s"; int run(String[] args) { if (log == null) { @@ -421,14 +407,54 @@ private JlinkConfiguration initJlinkConfig() throws BadArgs { } 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 doesn't include + // java.base. In that case take the JDK modules from the + // current run-time image. + if (finder.find("java.base").isEmpty()) { + isLinkFromRuntime = true; + ModuleFinder runtimeImageFinder = ModuleFinder.ofSystem(); + finder = combinedFinders(finder, runtimeImageFinder, options.limitMods, roots); + } + return new JlinkConfiguration(options.output, roots, finder, - options.modulePath.isEmpty(), + isLinkFromRuntime, options.ignoreModifiedRuntime); } + private ModuleFinder combinedFinders(ModuleFinder finder, + ModuleFinder runtimeImageFinder, Set limitMods, + Set roots) { + ModuleFinder combined = new ModuleFinder() { + + @Override + public Optional find(String name) { + Optional basic = runtimeImageFinder.find(name); + if (basic.isEmpty()) { + return finder.find(name); + } + return basic; + } + + @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 { if (options.output == null) { throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true); @@ -451,7 +477,7 @@ private void createImage(JlinkConfiguration config) throws Exception { // Then create the Plugin Stack ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration( taskHelper.getPluginsConfig(options.output, options.launchers, - imageProvider.targetPlatform, config), getMergedCliArgs(config.linkFromRuntimeImage())); + imageProvider.targetPlatform)); //Ask the stack to proceed stack.operate(imageProvider); @@ -510,15 +536,6 @@ public static ModuleFinder newModuleFinder(List paths, return finder; } - private static ModuleFinder moduleFinderFromPath(List paths, Runtime.Version version) { - if (Objects.requireNonNull(paths).isEmpty()) { - throw new IllegalArgumentException(taskHelper.getMessage("err.empty.module.path")); - } - - Path[] entries = paths.toArray(new Path[0]); - return ModulePath.of(version, true, entries); - } - private static void deleteDirectory(Path dir) throws IOException { Files.walkFileTree(dir, new SimpleFileVisitor() { @Override @@ -575,37 +592,25 @@ private static ImageHelper createImageProvider(JlinkConfiguration config, // Perform some setup for run-time image based links if (config.linkFromRuntimeImage()) { - // Check if the current run-time image has already been created using - // a run-time image link. If it is, fail the link as recursive - // links are not allowed. - try (InputStream in = JlinkTask.class.getModule().getResourceAsStream(RUNIMAGE_LINK_STAMP)) { - if (in != null) { - String msg = taskHelper.getMessage("err.runtime.link.recursive"); - throw new IllegalArgumentException(msg); - } + // Catch the case where we don't have a linkable runtime. In that + // case, fs_java.base_files doesn't exist in the jdk.jlink + // module. + String resourceName = String.format(RESPATH_PATTERN, "java.base"); + InputStream inStream = JlinkTask.class.getModule().getResourceAsStream(resourceName); + if (inStream == null) { + // Only linkable runtimes have those resources. Abort otherwise. + String msg = taskHelper.getMessage("err.runtime.link.not.linkable.runtime"); + throw new IllegalArgumentException(msg); + } + // Disallow a runtime-image-based-link with jdk.jlink included + if (cf.findModule(JLINK_MOD_NAME).isPresent()) { + String msg = taskHelper.getMessage("err.runtime.link.jdk.jlink.prohibited"); + throw new IllegalArgumentException(msg); } // Print info message when a run-image link is being performed if (log != null) { - Path detailsFile = Path.of("runtime-jlink-details.txt"); - String verboseHint = " " + taskHelper.getMessage("runtime.link.info.hint", detailsFile.toString()); - if (verbose) { - // Don't mention the hint if we already use --verbose. - verboseHint = ""; - } - log.println(taskHelper.getMessage("runtime.link.info", verboseHint)); - List mergedCLIArgs = getMergedCliArgs(config.linkFromRuntimeImage()); - if (verbose) { - // Log to standard out with detailed info - logPackagedModuleEquivalent(log, mergedCLIArgs, opts, verbose); - } else { - // Log to a file 'runtime-jlink-details.txt' with the details - // how the runtime-derived image can get produced using - // packaged modules - try (PrintWriter fileOut = new PrintWriter(new FileOutputStream(detailsFile.toFile()))) { - logPackagedModuleEquivalent(fileOut, mergedCLIArgs, opts, verbose); - } - } + log.println(taskHelper.getMessage("runtime.link.info")); } } @@ -669,14 +674,13 @@ private static ImageHelper createImageProvider(JlinkConfiguration config, .orElse(Runtime.version()); Set archives = mods.entrySet().stream() - .map(e -> config.linkFromRuntimeImage() ? new JRTArchive(e.getKey(), e.getValue(), !config.ignoreModifiedRuntime()) - : newArchive(e.getKey(), e.getValue(), version, ignoreSigning)) + .map(e -> newArchive(e.getKey(), e.getValue(), version, ignoreSigning, config)) .collect(Collectors.toSet()); return new ImageHelper(archives, targetPlatform, retainModulesPath); } - private static Archive newArchive(String module, Path path, Runtime.Version version, boolean ignoreSigning) { + private static Archive newArchive(String module, Path path, Runtime.Version version, boolean ignoreSigning, JlinkConfiguration config) { if (path.toString().endsWith(".jmod")) { return new JmodArchive(module, path); } else if (path.toString().endsWith(".jar")) { @@ -703,6 +707,12 @@ private static Archive newArchive(String module, Path path, Runtime.Version vers } } return modularJarArchive; + } else if (config.linkFromRuntimeImage()) { + // This is after jmod and modular jar branches, since, in runtime link + // mode custom - JDK external modules - are only supported via the + // module path. Directory module paths are not supported since those + // clash with JRT-FS based archives of JRTArchive. + return new JRTArchive(module, path, !config.ignoreModifiedRuntime()); } else if (Files.isDirectory(path)) { Path modInfoPath = path.resolve("module-info.class"); if (Files.isRegularFile(modInfoPath)) { @@ -727,113 +737,6 @@ private static String findModuleName(Path modInfoPath) { } } - /** - * Log a message when a run-time image link is being performed and mention - * the equivalent packaged-module based link. - * - * @param logWriter - * The log to print to. - * @param mergedCLI - * The merged command line parameters of the persisted link of - * the run-time image being used and the current command line - * arguments. - * @param opts - * The parsed options of the current command line arguments. - */ - private static void logPackagedModuleEquivalent(PrintWriter logWriter, - List mergedCLI, OptionsValues opts, boolean isVerbose) { - // parse options, produce plugins maps. - TaskHelper scratchTaskHelper = new TaskHelper(JLINK_BUNDLE); - OptionsHelper scratchOptionsHelper = scratchTaskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions); - JlinkTask scratch = new JlinkTask(true); - - Map>> pluginMaps = null; - try { - scratchOptionsHelper.handleOptions(scratch, mergedCLI.toArray(new String[] {})); - pluginMaps = scratchTaskHelper.getPluginMaps(); - } catch (BadArgs e) { - throw new AssertionError("handling of scratch options failed", e); - } - List forwardingPlugins = pluginMaps.keySet() - .stream() - .filter(p -> p.runTimeImageLinkPersistent()) - .toList(); - List jlinkCmd = new ArrayList<>(); - jlinkCmd.add("jlink"); - // Iterate over recognized opts to figure out options used - if (opts.bindServices) { - jlinkCmd.add("--bind-services"); - } - if (opts.suggestProviders) { - jlinkCmd.add("--suggest-providers"); - } - if (opts.ignoreSigning) { - jlinkCmd.add("--ignore-signing"); - } - // --add-modules is a required option, but the JLink API calls this with - // dummy options - if (!opts.addMods.isEmpty()) { - jlinkCmd.add("--add-modules"); - jlinkCmd.add(opts.addMods.stream().collect(Collectors.joining(","))); - } - if (!opts.limitMods.isEmpty()) { - jlinkCmd.add("--limit-modules"); - jlinkCmd.add(opts.limitMods.stream().collect(Collectors.joining(","))); - } - // Launchers carry forward, so we need to use the scratch JlinkTask - if (!scratch.options.launchers.isEmpty()) { - for (Map.Entry entry: scratch.options.launchers.entrySet()) { - jlinkCmd.add("--launcher"); - jlinkCmd.add(entry.getKey() + "=" + entry.getValue()); - } - } - String outputPath = ""; - if (opts.output != null) { - outputPath = opts.output.toString(); - jlinkCmd.add("--output"); - jlinkCmd.add(outputPath); - } - logWriter.println(taskHelper.getMessage("runtime.link.equivalent.packaged.modules", outputPath)); - logWriter.print(" "); - logWriter.print(jlinkCmd.stream().collect(Collectors.joining(" "))); - for (Plugin p: forwardingPlugins) { - List> configs = pluginMaps.get(p); - for (Map config: configs) { - String value = config.get(p.getName()); - // Work-around for --dedup-legal-notices which auto-enables, - // carries forward and doesn't allow to be run without argument - // value from the CLI. - if (p instanceof LegalNoticeFilePlugin && value == null) { - continue; - } - logWriter.print(" --" + p.getName()); - if (value != null) { - logWriter.print(" " + value); - } - } - } - logWriter.println(); - if (isVerbose) { - logWriter.println(); // empty line to separate from modules output - } - } - - private static List getMergedCliArgs(boolean isRunTimeImageLink) throws IOException { - // First read in the stored CLI args that were used for the input - // run-time image - List merged = new ArrayList<>(); - if (isRunTimeImageLink) { - try (InputStream in = JlinkTask.class.getModule().getResourceAsStream(CLI_RESOURCE_FILE)) { - CommandLine.loadCmdFile(Objects.requireNonNull(in, "Old CLI args not being tracked in jimage"), - merged); - } - } - for (String arg: optionsHelper.getInputCommand()) { - merged.add(arg); - } - return Collections.unmodifiableList(merged); - } - /* * Returns a ModuleFinder that limits observability to the given root * modules, their transitive dependences, plus a set of other modules. diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourceDiff.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourceDiff.java new file mode 100644 index 0000000000000..5fe25b5642ea1 --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourceDiff.java @@ -0,0 +1,270 @@ +/* + * 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.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.List; +import java.util.Objects; + +/** + * Class representing a difference between 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. 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 IllegalStateException("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 IllegalStateException("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 IllegalStateException("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); + } + } + } catch (IOException e) { + e.printStackTrace(); + throw e; + } + } + + /** + * 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; + 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(); + diffs = new ArrayList<>(numItems); + for (int i = 0; i < numItems; i++) { + Kind k = Kind.fromShort(din.readShort()); + int numBytes = din.readInt(); + byte[] buf = readNumBytesFromStream(din, numBytes); + String name = new String(buf, StandardCharsets.UTF_8); + numBytes = din.readInt(); + byte[] resBytes = null; + if (numBytes != 0) { + resBytes = readNumBytesFromStream(din, numBytes); + } + Builder builder = new Builder(); + builder.setKind(k) + .setName(name); + if (resBytes != null) { + builder.setResourceBytes(resBytes); + } + diffs.add(builder.build()); + } + } + return diffs; + } + + private static byte[] readNumBytesFromStream(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; + } +} 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 5e3ebd49c32ca..2b4e6ca0a976c 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 @@ -28,35 +28,34 @@ 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.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; +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.Map; -import java.util.Map.Entry; -import java.util.MissingResourceException; import java.util.ResourceBundle; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.MissingResourceException; +import java.util.Comparator; + import jdk.tools.jlink.builder.DefaultImageBuilder; import jdk.tools.jlink.builder.ImageBuilder; -import jdk.tools.jlink.internal.Jlink.JlinkConfiguration; import jdk.tools.jlink.internal.Jlink.PluginsConfiguration; import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin; import jdk.tools.jlink.internal.plugins.DefaultStripDebugPlugin; import jdk.tools.jlink.internal.plugins.ExcludeJmodSectionPlugin; -import jdk.tools.jlink.internal.plugins.ExcludePlugin; import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; -import jdk.tools.jlink.internal.plugins.SystemModulesPlugin; import jdk.tools.jlink.plugin.Plugin; import jdk.tools.jlink.plugin.Plugin.Category; +import jdk.tools.jlink.plugin.PluginException; /** * @@ -411,7 +410,7 @@ private PluginOption getOption(String name) throws BadArgs { } private PluginsConfiguration getPluginsConfig(Path output, Map launchers, - Platform targetPlatform, JlinkConfiguration config) + Platform targetPlatform) throws IOException, BadArgs { if (output != null) { if (Files.exists(output)) { @@ -420,47 +419,6 @@ private PluginsConfiguration getPluginsConfig(Path output, Map l } } - // if we perform a run-time based link, add relevant exclude - // patterns, so as to match the packaged-modules-based link - if (config.linkFromRuntimeImage()) { - Plugin systemModulesPlugin = null; - Plugin excludeResourcePlugin = null; - List excludePatterns = new ArrayList<>(); - for (Plugin p: pluginToMaps.keySet()) { - if (p instanceof ExcludePlugin) { - excludeResourcePlugin = p; - } - if (p instanceof SystemModulesPlugin) { - systemModulesPlugin = p; - } - if (p.getExcludePatterns() != null) { - excludePatterns.addAll(p.getExcludePatterns()); - } - } - String additionalPatterns = excludePatterns.stream().collect(Collectors.joining(",")); - List> excludeResConfig = null; - if (excludeResourcePlugin == null) { - // no existing 'exclude-resources' setting - excludeResourcePlugin = PluginRepository.getPlugin("exclude-resources", ModuleLayer.boot()); - excludeResConfig = new ArrayList<>(); - excludeResConfig.add(Map.of("exclude-resources", additionalPatterns)); - pluginToMaps.put(excludeResourcePlugin, excludeResConfig); - } else { - excludeResConfig = pluginToMaps.get(excludeResourcePlugin); - // currently last exclude-resources wins - Map lastConfig = excludeResConfig.get(excludeResConfig.size() - 1); - String existingPattern = lastConfig.get("exclude-resources"); - lastConfig.put("exclude-resources", existingPattern + "," + additionalPatterns); - excludeResConfig.set(excludeResConfig.size() - 1, lastConfig); - } - // If the system modules plug-in is disabled, we fail the link - // as the SystemModulesMap class isn't guaranteed to be suitable - // for the to-be produced jimage. - if (systemModulesPlugin == null) { - throw new IllegalArgumentException("Disabling system-modules plugin for a run-time image based link is not allowed."); - } - } - List pluginsList = new ArrayList<>(); Set seenPlugins = new HashSet<>(); for (Entry>> entry : pluginToMaps.entrySet()) { @@ -644,7 +602,6 @@ public void listPlugins() { getPlugins(pluginOptions.pluginsLayer); pluginList.stream() - .filter((Plugin plugin) -> !plugin.isHidden()) .sorted(Comparator.comparing((Plugin plugin) -> plugin.getUsage().isEmpty(), (Boolean res1, Boolean res2) -> Boolean.compare(res2,res1)) .thenComparing(Plugin::getName) @@ -753,13 +710,9 @@ public void warning(String key, Object... args) { } public PluginsConfiguration getPluginsConfig(Path output, Map launchers, - Platform targetPlatform, JlinkConfiguration config) + Platform targetPlatform) throws IOException, BadArgs { - return pluginOptions.getPluginsConfig(output, launchers, targetPlatform, config); - } - - Map>> getPluginMaps() { - return pluginOptions.pluginToMaps; + return pluginOptions.getPluginsConfig(output, launchers, targetPlatform); } public void showVersion(boolean full) { diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/AddOptionsPlugin.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/AddOptionsPlugin.java index beddeffe1c51b..26c70f3f61712 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/AddOptionsPlugin.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/AddOptionsPlugin.java @@ -25,24 +25,14 @@ package jdk.tools.jlink.internal.plugins; -import java.util.List; - /** * Plugin to add VM command-line options, by storing them in a resource * that's read by the VM at startup */ public final class AddOptionsPlugin extends AddResourcePlugin { - private static final String OPTS_FILE = "/java.base/jdk/internal/vm/options"; - public AddOptionsPlugin() { - super("add-options", OPTS_FILE); - } - - // Filter the file we create for run-time image based links - @Override - public List getExcludePatterns() { - return List.of("glob:" + AddOptionsPlugin.OPTS_FILE); + super("add-options", "/java.base/jdk/internal/vm/options"); } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/JlinkResourcesListPlugin.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/CreateLinkableRuntimePlugin.java similarity index 61% rename from src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/JlinkResourcesListPlugin.java rename to src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/CreateLinkableRuntimePlugin.java index 17f724b83b1f0..072f6cf845ae8 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/JlinkResourcesListPlugin.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/CreateLinkableRuntimePlugin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -25,71 +25,72 @@ package jdk.tools.jlink.internal.plugins; +import static jdk.tools.jlink.internal.JlinkTask.DIFF_PATTERN; +import static jdk.tools.jlink.internal.JlinkTask.RESPATH_PATTERN; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import jdk.tools.jlink.internal.JRTArchive; -import jdk.tools.jlink.internal.JlinkCLIArgsListener; import jdk.tools.jlink.internal.Platform; +import jdk.tools.jlink.internal.ResourceDiff; import jdk.tools.jlink.plugin.ResourcePool; import jdk.tools.jlink.plugin.ResourcePoolBuilder; import jdk.tools.jlink.plugin.ResourcePoolEntry; import jdk.tools.jlink.plugin.ResourcePoolModule; -import static jdk.tools.jlink.internal.JlinkTask.RUNIMAGE_LINK_STAMP; -import static jdk.tools.jlink.internal.JlinkTask.CLI_RESOURCE_FILE; -import static jdk.tools.jlink.internal.JlinkTask.RESPATH_PATTERN; - /** - * Plugin to collect resources from jmod which aren't classes or - * resources. Needed for the the run-time image based jlink. + * Plugin to produce a runtime-linkable JDK image. It does the following: + * + *
    + *
  • + * It tracks resources per module and saves the list in 'fs_$M_files' in the + * jdk.jlink module. + *
  • + *
  • + * It adds a serialized resource diff to the jdk.jlink module so as to be able + * to reconstruct the packaged modules equivalent view when performing runtime + * links. + *
  • + *
*/ -public final class JlinkResourcesListPlugin extends AbstractPlugin implements JlinkCLIArgsListener { - private static final String NAME = "add-run-image-resources"; +public final class CreateLinkableRuntimePlugin extends AbstractPlugin { + private static final String PLUGIN_NAME = "create-linkable-runtime"; private static final String JLINK_MOD_NAME = "jdk.jlink"; // This resource is being used in JLinkTask which passes its contents to - // RunImageArchive for further processing. + // JRTArchive for further processing. private static final String RESPATH = "/" + JLINK_MOD_NAME + "/" + RESPATH_PATTERN; - private static final String CLI_RESOURCE = "/" + JLINK_MOD_NAME + "/" + CLI_RESOURCE_FILE; - private static final Pattern WHITESPACE_PATTERN = Pattern.compile(".*\\s.*"); + private static final String DIFF_PATH = "/" + JLINK_MOD_NAME + "/" + DIFF_PATTERN; private static final byte[] EMPTY_RESOURCE_BYTES = new byte[] {}; - // 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 final Map> nonClassResEntries; + private String diffFile; - private List commands; - - public JlinkResourcesListPlugin() { - super(NAME); + public CreateLinkableRuntimePlugin() { + super(PLUGIN_NAME); this.nonClassResEntries = new ConcurrentHashMap<>(); } @Override - public boolean isHidden() { - return true; // Don't show in --list-plugins output + public void configure(Map config) { + String v = config.get(PLUGIN_NAME); + if (v == null) + throw new AssertionError(); + diffFile = v; } @Override @@ -101,40 +102,52 @@ public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { Platform targetPlatform = getTargetPlatform(in); in.transformAndCopy(e -> recordAndFilterEntry(e, targetPlatform), out); addModuleResourceEntries(in, out); - addCLIResource(out); + addResourceDiffFiles(in, out); } else { - in.transformAndCopy(Function.identity(), out); + throw new IllegalStateException("jdk.jlink module not in list of modules for target image"); } return out.build(); } - private void addCLIResource(ResourcePoolBuilder out) { - out.add(ResourcePoolEntry.create(CLI_RESOURCE, getCliBytes())); - } - - private byte[] getCliBytes() { - StringBuilder builder = new StringBuilder(); - for (String s: commands) { - Matcher m = WHITESPACE_PATTERN.matcher(s); - if (m.matches()) { - // Quote arguments containing whitespace - builder.append("\""); - builder.append(s); - builder.append("\""); - } else { - builder.append(s); + private void addResourceDiffFiles(ResourcePool in, ResourcePoolBuilder out) { + Map> diffs = readResourceDiffs(); + // Create a resource file with the delta to the packaged-modules view + in.moduleView().modules().forEach(m -> { + String modName = m.descriptor().name(); + String resFile = String.format(DIFF_PATH, modName); + List perModDiff = diffs.get(modName); + // Not every module will have a diff + if (perModDiff == null) { + perModDiff = Collections.emptyList(); } - builder.append(" "); - } - builder.append("\n"); - return builder.toString().getBytes(StandardCharsets.UTF_8); + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try { + ResourceDiff.write(perModDiff, bout); + } catch (IOException e) { + throw new IllegalStateException("Failed to write per module diff"); + } + out.add(ResourcePoolEntry.create(resFile, bout.toByteArray())); + }); } - // Filter the resource we add. - @Override - public List getExcludePatterns() { - return List.of("glob:" + CLI_RESOURCE, - "regex:/jdk\\.jlink/" + String.format(RESPATH_PATTERN, ".*")); + private Map> readResourceDiffs() { + List resDiffs = null; + try (FileInputStream fin = new FileInputStream(new File(diffFile))) { + resDiffs = ResourceDiff.read(fin); + } catch (IOException e) { + throw new IllegalStateException("Failed to read resource diff file: " + diffFile); + } + 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); + }); + return modToDiff; } private Platform getTargetPlatform(ResourcePool in) { @@ -182,25 +195,20 @@ private ResourcePoolEntry recordAndFilterEntry(ResourcePoolEntry entry, Platform @Override public Set getState() { - return EnumSet.of(State.AUTO_ENABLED, State.FUNCTIONAL); + return EnumSet.of(State.FUNCTIONAL); } @Override public boolean hasArguments() { - return false; + return true; } @Override public Category getType() { // Ensure we run in a later stage as we need to generate - // SHA-512 sums for non-(class/resource) files. The jmod_resources + // SHA-512 sums for non-(class/resource) files. The fs_$module_files // files can be considered meta-info describing the universe we - // draft from. + // draft from (together with the jimage and respective diff_$module files). return Category.METAINFO_ADDER; } - - @Override - public void process(List commands) { - this.commands = commands; - } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/GenerateJLIClassesPlugin.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/GenerateJLIClassesPlugin.java index 32b2398e5552d..dcc9c3acdea41 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/GenerateJLIClassesPlugin.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/GenerateJLIClassesPlugin.java @@ -31,7 +31,6 @@ import java.io.InputStreamReader; import java.nio.file.Files; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Stream; @@ -155,22 +154,6 @@ public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { return out.build(); } - // We generate these JLI Species classes so they would be in the run-time - // image we derive from if we don't filter them for run-time image based - // links. This filter corresponds to the set of classes part of the - // jmods. - @Override - public List getExcludePatterns() { - return List.of("regex:/java\\.base/java/lang/invoke/BoundMethodHandle\\$Species_(?:D|DL|I|IL|LJ|LL).*\\.class"); - } - - // This plugin doesn't persist, since generated classes are filtered for - // run-time image based links. - @Override - public boolean runTimeImageLinkPersistent() { - return false; - } - private static final String DIRECT_METHOD_HOLDER_ENTRY = "/java.base/java/lang/invoke/DirectMethodHandle$Holder.class"; private static final String DELEGATING_METHOD_HOLDER_ENTRY = diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SaveJlinkArgfilesPlugin.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SaveJlinkArgfilesPlugin.java index 2146dbfda750a..3cfc957896505 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SaveJlinkArgfilesPlugin.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SaveJlinkArgfilesPlugin.java @@ -38,7 +38,6 @@ import java.util.function.Function; import java.util.stream.Collectors; -import jdk.tools.jlink.internal.JlinkTask; import jdk.tools.jlink.plugin.PluginException; import jdk.tools.jlink.plugin.ResourcePool; import jdk.tools.jlink.plugin.ResourcePoolBuilder; @@ -105,10 +104,4 @@ public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { savedOptions)); return out.build(); } - - // Filter the file we create for run-time image based links - @Override - public List getExcludePatterns() { - return List.of("glob:/jdk.jlink/" + JlinkTask.OPTIONS_RESOURCE); - } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SystemModulesPlugin.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SystemModulesPlugin.java index 0364c0bcf97eb..40693b33d6db2 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SystemModulesPlugin.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/plugins/SystemModulesPlugin.java @@ -24,33 +24,13 @@ */ package jdk.tools.jlink.internal.plugins; -import static java.lang.classfile.ClassFile.ACC_FINAL; -import static java.lang.classfile.ClassFile.ACC_PUBLIC; -import static java.lang.classfile.ClassFile.ACC_STATIC; -import static java.lang.classfile.ClassFile.ACC_SUPER; -import static java.lang.constant.ConstantDescs.CD_List; -import static java.lang.constant.ConstantDescs.CD_Map; -import static java.lang.constant.ConstantDescs.CD_Object; -import static java.lang.constant.ConstantDescs.CD_Set; -import static java.lang.constant.ConstantDescs.CD_String; -import static java.lang.constant.ConstantDescs.CD_boolean; -import static java.lang.constant.ConstantDescs.CD_byte; -import static java.lang.constant.ConstantDescs.CD_int; -import static java.lang.constant.ConstantDescs.CD_void; -import static java.lang.constant.ConstantDescs.INIT_NAME; -import static java.lang.constant.ConstantDescs.MTD_void; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.lang.classfile.ClassBuilder; -import java.lang.classfile.ClassFile; -import java.lang.classfile.CodeBuilder; -import java.lang.classfile.TypeKind; -import java.lang.classfile.attribute.ModulePackagesAttribute; import java.lang.constant.ClassDesc; import java.lang.constant.ConstantDesc; +import static java.lang.constant.ConstantDescs.*; import java.lang.constant.MethodTypeDesc; import java.lang.module.Configuration; import java.lang.module.ModuleDescriptor; @@ -86,13 +66,21 @@ import jdk.internal.module.Checks; import jdk.internal.module.DefaultRoots; +import jdk.internal.module.Modules; import jdk.internal.module.ModuleHashes; import jdk.internal.module.ModuleInfo.Attributes; import jdk.internal.module.ModuleInfoExtender; import jdk.internal.module.ModuleReferenceImpl; import jdk.internal.module.ModuleResolution; import jdk.internal.module.ModuleTarget; -import jdk.internal.module.Modules; + +import java.lang.classfile.attribute.ModulePackagesAttribute; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassFile; +import java.lang.classfile.TypeKind; +import static java.lang.classfile.ClassFile.*; +import java.lang.classfile.CodeBuilder; + import jdk.tools.jlink.internal.ModuleSorter; import jdk.tools.jlink.plugin.PluginException; import jdk.tools.jlink.plugin.ResourcePool; @@ -188,22 +176,6 @@ public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { return out.build(); } - // Certain system module classes get generated and SystemModulesMap replaced. - // Filter previously generated classes so the set of classes match the set - // of the packaged modules link. - @Override - public List getExcludePatterns() { - return List.of("regex:/java\\.base/jdk/internal/module/SystemModules\\$.*\\.class"); - } - - // This plugin doesn't persist, since generated classes are filtered for - // run-time image based links and running without --system-modules on a - // run-time image based link is not allowed. - @Override - public boolean runTimeImageLinkPersistent() { - return false; - } - /** * Validates and transforms the module-info.class files in the modules, adding * the ModulePackages class file attribute if needed. diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/plugin/Plugin.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/plugin/Plugin.java index e1da73d3ef22c..11a5d63005143 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/plugin/Plugin.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/plugin/Plugin.java @@ -25,11 +25,9 @@ package jdk.tools.jlink.plugin; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Set; -import jdk.tools.jlink.internal.plugins.ExcludePlugin; import jdk.tools.jlink.internal.plugins.PluginsResourceBundle; /** @@ -168,44 +166,6 @@ public default boolean hasRawArgument() { return false; } - /** - * Determine whether or not the plugin is hidden from - * {@code jlink --list-plugins} output. - * - * @return true if the plugin needs to be hidden from --list-plugins - */ - public default boolean isHidden() { - return false; - } - - /** - * A list of exclude patterns suitable to be fed to the - * {@link ExcludePlugin}. The list of patterns should be such that - * the classes and resource list will be the same as before the - * plugin ran. - * - * @return A list of glob or regex patterns suitable for the - * exclude-resource plugin. - */ - public default List getExcludePatterns() { - return null; - } - - /** - * Determined whether or not application of the plugin changes binary files - * in the resulting image on the filesystem or the plugin changes classes or - * resources in the resulting jimage and there is no way to undo this - * operation. This is being used for the run-time image based link so as to - * craft an equivalent CLI command of a link using packaged modules. - * - * @return {@code true} if the transformation cannot be undone and carries - * forward in a subsequent jlink using the run-time image. - * {@code false} otherwise. - */ - public default boolean runTimeImageLinkPersistent() { - return getType() == Category.FILTER || getType() == Category.TRANSFORMER; - } - /** * The plugin argument(s) description. * @return The argument(s) description. 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 e76a8203c8798..c6896a79df6e0 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 @@ -115,8 +115,8 @@ main.extended.help.footer=\ error.prefix=Error: warn.prefix=Warning: -err.runtime.link.recursive=Module path to the JDK packaged modules must be specified. \ -Run-time image based linking is not supported as $java.home was already created from a run-time image. +err.runtime.link.not.linkable.runtime=The current run-time image does not support run-time linking. +err.runtime.link.jdk.jlink.prohibited=Including jdk.jlink module for run-time image based links is not allowed. err.runtime.link.packaged.mods=--keep-packaged-modules is not allowed for run-time image based links. err.empty.module.path=empty module path err.jlink.version.mismatch=jlink version {0}.{1} does not match target java.base version {2}.{3} @@ -161,7 +161,5 @@ no.suggested.providers=--bind-services option is specified. No additional provid suggested.providers.header=Suggested providers providers.header=Providers -runtime.link.info=Linking based on the current run-time image.{0} -runtime.link.info.hint=Details for reproducing the image using JDK packaged modules are in file {0} or are shown when using --verbose. -runtime.link.jprt.path.extra=(run-time image) -runtime.link.equivalent.packaged.modules=''{0}'' is linked equivalent to using jlink with JDK packaged modules as: +runtime.link.info=Linking based on the current run-time image. +runtime.link.jprt.path.extra=(run-time image) \ No newline at end of file diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties index 0d683edd83f27..7c118ef18bb80 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/resources/plugins.properties @@ -251,6 +251,19 @@ vendor-vm-bug-url.usage=\ \ into the build. The URL displayed in VM error\n\ \ logs will be . +create-linkable-runtime.argument= + +create-linkable-runtime.description=\ +Create a linkable run-time image given a serialized diff file (as compared to\n\ +the contents - classes and resources - of the packaged modules). + +create-linkable-runtime.usage=\ +\ --create-linkable-runtime \n\ +\ Creates a linkable run-time image so that \n\ +\ the current jimage (which includes jdk.jlink)\n\ +\ together with natives from the filesystem\n\ +\ can be used to create derivative images.\n\ + vm.argument= vm.description=\ diff --git a/src/jdk.jlink/share/classes/module-info.java b/src/jdk.jlink/share/classes/module-info.java index c7a110d96349c..2ab28f6a670f2 100644 --- a/src/jdk.jlink/share/classes/module-info.java +++ b/src/jdk.jlink/share/classes/module-info.java @@ -84,6 +84,6 @@ jdk.tools.jlink.internal.plugins.VendorVMBugURLPlugin, jdk.tools.jlink.internal.plugins.VendorVersionPlugin, jdk.tools.jlink.internal.plugins.CDSPlugin, - jdk.tools.jlink.internal.plugins.JlinkResourcesListPlugin, + jdk.tools.jlink.internal.plugins.CreateLinkableRuntimePlugin, jdk.tools.jlink.internal.plugins.SaveJlinkArgfilesPlugin; } diff --git a/test/hotspot/jtreg/TEST.ROOT b/test/hotspot/jtreg/TEST.ROOT index 4c9a28a3076bf..9847fe8a015fc 100644 --- a/test/hotspot/jtreg/TEST.ROOT +++ b/test/hotspot/jtreg/TEST.ROOT @@ -87,7 +87,8 @@ requires.properties= \ vm.musl \ vm.flagless \ docker.support \ - jdk.containerized + jdk.containerized \ + jlink.runtime.linkable # Minimum jtreg version requiredVersion=7.3.1+1 diff --git a/test/jdk/TEST.ROOT b/test/jdk/TEST.ROOT index 25ef8250e4d45..ad24d72c0647f 100644 --- a/test/jdk/TEST.ROOT +++ b/test/jdk/TEST.ROOT @@ -102,7 +102,8 @@ requires.properties= \ docker.support \ release.implementor \ jdk.containerized \ - jdk.foreign.linker + jdk.foreign.linker \ + jlink.runtime.linkable # Minimum jtreg version requiredVersion=7.3.1+1 diff --git a/test/jdk/tools/jlink/ImageFileCreatorTest.java b/test/jdk/tools/jlink/ImageFileCreatorTest.java index b280674c25449..84f77340f42c1 100644 --- a/test/jdk/tools/jlink/ImageFileCreatorTest.java +++ b/test/jdk/tools/jlink/ImageFileCreatorTest.java @@ -34,12 +34,11 @@ 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; @@ -222,7 +221,7 @@ public void storeFiles(ResourcePool content) { }; ImagePluginStack stack = new ImagePluginStack(noopBuilder, Collections.emptyList(), - null, false, null); + null, false); ImageFileCreator.create(archives, ByteOrder.nativeOrder(), stack); } diff --git a/test/jdk/tools/jlink/JLinkTest.java b/test/jdk/tools/jlink/JLinkTest.java index a069fe3e3cb27..e897badad133c 100644 --- a/test/jdk/tools/jlink/JLinkTest.java +++ b/test/jdk/tools/jlink/JLinkTest.java @@ -27,15 +27,17 @@ import java.lang.module.ModuleDescriptor; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.spi.ToolProvider; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import jdk.tools.jlink.internal.PluginRepository; import jdk.tools.jlink.plugin.Plugin; +import jdk.tools.jlink.internal.PluginRepository; import tests.Helper; import tests.JImageGenerator; @@ -93,8 +95,7 @@ public static void main(String[] args) throws Exception { // number of built-in plugins List builtInPlugins = new ArrayList<>(); builtInPlugins.addAll(PluginRepository.getPlugins(ModuleLayer.boot())); - // AddRunImageResourcesPlugin is hidden from --list-plugins - totalPlugins = builtInPlugins.stream().filter(p -> !p.isHidden()).collect(Collectors.toList()).size(); + totalPlugins = builtInPlugins.size(); // actual num. of plugins loaded from jdk.jlink module int actualJLinkPlugins = 0; for (Plugin p : builtInPlugins) { diff --git a/test/jdk/tools/jlink/JmodLess/AbstractJmodLessTest.java b/test/jdk/tools/jlink/JmodLess/AbstractLinkableRuntimeTest.java similarity index 79% rename from test/jdk/tools/jlink/JmodLess/AbstractJmodLessTest.java rename to test/jdk/tools/jlink/JmodLess/AbstractLinkableRuntimeTest.java index 49c07f5f5f7ab..bd57deb2a8cb7 100644 --- a/test/jdk/tools/jlink/JmodLess/AbstractJmodLessTest.java +++ b/test/jdk/tools/jlink/JmodLess/AbstractLinkableRuntimeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -21,12 +21,18 @@ * 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.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.function.Predicate; import java.util.stream.Collectors; @@ -34,14 +40,10 @@ import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; import tests.Helper; -import tests.JImageGenerator; -import tests.JImageGenerator.JLinkTask; import tests.JImageValidator; -public abstract class AbstractJmodLessTest { +public abstract class AbstractLinkableRuntimeTest { - protected static final String EXCLUDE_RESOURCE_GLOB_STAMP = "/" + jdk.tools.jlink.internal.JlinkTask.class.getModule().getName() + - "/" + jdk.tools.jlink.internal.JlinkTask.RUNIMAGE_LINK_STAMP; protected static final boolean DEBUG = true; public void run() throws Exception { @@ -101,9 +103,9 @@ protected OutputAnalyzer runJavaCmd(Path image, List options) throws Exc return out; } - protected Path createJavaImageJmodLess(BaseJlinkSpec baseSpec) throws Exception { + protected Path createJavaImageRuntimeLink(BaseJlinkSpec baseSpec) throws Exception { // create a base image only containing the jdk.jlink module and its transitive closure - Path jlinkJmodlessImage = createBaseJlinkImage(baseSpec); + Path runtimeJlinkImage = createRuntimeLinkImage(baseSpec); // On Windows jvm.dll is in 'bin' after the jlink Path libjvm = Path.of((isWindows() ? "bin" : "lib"), "server", System.mapLibraryName("jvm")); @@ -113,7 +115,7 @@ protected Path createJavaImageJmodLess(BaseJlinkSpec baseSpec) throws Exception .helper(baseSpec.getHelper()) .name(baseSpec.getName()) .validatingModule(baseSpec.getValidatingModule()) - .imagePath(jlinkJmodlessImage) + .imagePath(runtimeJlinkImage) .expectedLocation("/java.base/java/lang/String.class"); for (String m: baseSpec.getModules()) { builder.addModule(m); @@ -125,7 +127,7 @@ protected Path createJavaImageJmodLess(BaseJlinkSpec baseSpec) throws Exception } protected Path jlinkUsingImage(JlinkSpec spec) throws Exception { - return jlinkUsingImage(spec, new NoopOutputAnalyzerHandler()); + return jlinkUsingImage(spec, new RuntimeLinkOutputAnalyzerHandler()); } protected Path jlinkUsingImage(JlinkSpec spec, OutputAnalyzerHandler handler) throws Exception { @@ -147,6 +149,12 @@ protected Path jlinkUsingImage(JlinkSpec spec, OutputAnalyzerHandler handler, Pr 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: jmod-less jlink command: " + jlinkCmd.stream().collect( Collectors.joining(" "))); @@ -185,31 +193,55 @@ protected Path jlinkUsingImage(JlinkSpec spec, OutputAnalyzerHandler handler, Pr return targetImageDir; } - protected Path createBaseJlinkImage(BaseJlinkSpec baseSpec) throws Exception { - // Jlink an image including jdk.jlink (i.e. the jlink tool). The - // result must not contain a jmods directory. - Path jlinkJmodlessImage = baseSpec.getHelper().createNewImageDir(baseSpec.getName() + "-jlink"); - JLinkTask task = JImageGenerator.getJLinkTask(); - if (baseSpec.getModules().contains("leaf1")) { - task.modulePath(baseSpec.getHelper().getJmodDir().toString()); - } - task.output(jlinkJmodlessImage); - for (String module: baseSpec.getModules()) { - task.addMods(module); - } - if (!baseSpec.getModules().contains("ALL-MODULE-PATH")) { - task.addMods("jdk.jlink"); // needed for the recursive jlink - } - for (String opt: baseSpec.getExtraOptions()) { - task.option(opt); - } - task.option("--verbose") - .call().assertSuccess(); - // Verify the base image is actually jmod-less - if (Files.exists(jlinkJmodlessImage.resolve("jmods"))) { + /** + * Prepares the test for execution. This assumes the current jimage is + * runtime-linkable. However, since the 'jmods' dir is present (default jmods + * module path), it needs to get removed to provoke a runtime link. + * + * @param baseSpec + * @return A path to a JDK image which is prepared for runtime linking. + * @throws Exception + */ + protected Path createRuntimeLinkImage(BaseJlinkSpec baseSpec) throws Exception { + Path runtimeJlinkImage = baseSpec.getHelper().createNewImageDir(baseSpec.getName() + "-jlink"); + copyJDKTreeWithoutJmods(runtimeJlinkImage); + // Verify the base image is actually without packaged modules + if (Files.exists(runtimeJlinkImage.resolve("jmods"))) { throw new AssertionError("Must not contain 'jmods' directory"); } - return jlinkJmodlessImage; + return runtimeJlinkImage; + } + + private void copyJDKTreeWithoutJmods(Path runtimeJlinkImage) throws Exception { + Files.createDirectory(runtimeJlinkImage); + String javaHome = System.getProperty("java.home"); + Path root = Path.of(javaHome); + Files.walkFileTree(root, new SimpleFileVisitor() { + @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 = runtimeJlinkImage.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, runtimeJlinkImage.resolve(relative), StandardCopyOption.REPLACE_EXISTING); + return FileVisitResult.CONTINUE; + } + }); + } private List parseListMods(String output) throws Exception { @@ -334,11 +366,13 @@ static class JlinkSpec { 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 extraJlinkOpts, + List modulePath) { this.imageToUse = imageToUse; this.helper = helper; this.name = name; @@ -348,6 +382,7 @@ static class JlinkSpec { this.unexpectedLocations = unexpectedLocations; this.expectedFiles = expectedFiles; this.extraJlinkOpts = extraJlinkOpts; + this.modulePath = modulePath; } public Path getImageToUse() { @@ -385,6 +420,10 @@ public String[] getExpectedFiles() { public List getExtraJlinkOpts() { return extraJlinkOpts; } + + public List getModulePath() { + return modulePath; + } } static class JlinkSpecBuilder { @@ -397,6 +436,7 @@ static class JlinkSpecBuilder { List unexpectedLocations = new ArrayList<>(); List expectedFiles = new ArrayList<>(); List extraJlinkOpts = new ArrayList<>(); + List modulePath = new ArrayList<>(); JlinkSpec build() { if (imageToUse == null) { @@ -411,7 +451,16 @@ JlinkSpec build() { 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); + return new JlinkSpec(imageToUse, + helper, + name, + modules, + validatingModule, + expectedLocations, + unexpectedLocations, + expectedFiles.toArray(new String[0]), + extraJlinkOpts, + modulePath); } JlinkSpecBuilder imagePath(Path image) { @@ -439,6 +488,11 @@ JlinkSpecBuilder validatingModule(String module) { return this; } + JlinkSpecBuilder addModulePath(String modulePath) { + this.modulePath.add(modulePath); + return this; + } + JlinkSpecBuilder expectedLocation(String location) { expectedLocations.add(location); return this; @@ -466,11 +520,11 @@ static abstract class OutputAnalyzerHandler { } - static class NoopOutputAnalyzerHandler extends OutputAnalyzerHandler { + static class RuntimeLinkOutputAnalyzerHandler extends OutputAnalyzerHandler { @Override public void handleAnalyzer(OutputAnalyzer out) { - // nothing + out.shouldContain("Linking based on the current run-time image."); } } diff --git a/test/jdk/tools/jlink/JmodLess/AddOptionsTest.java b/test/jdk/tools/jlink/JmodLess/AddOptionsTest.java index 1cfe936584f11..e9adbf2ae8974 100644 --- a/test/jdk/tools/jlink/JmodLess/AddOptionsTest.java +++ b/test/jdk/tools/jlink/JmodLess/AddOptionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -30,8 +30,8 @@ /* * @test - * @summary Test --add-options jlink plugin in jmod-less mode - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @summary Test --add-options jlink plugin in runtime image link mode + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -43,7 +43,7 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g AddOptionsTest */ -public class AddOptionsTest extends AbstractJmodLessTest { +public class AddOptionsTest extends AbstractLinkableRuntimeTest { public static void main(String[] args) throws Exception { AddOptionsTest test = new AddOptionsTest(); @@ -52,7 +52,7 @@ public static void main(String[] args) throws Exception { @Override void runTest(Helper helper) throws Exception { - Path finalImage = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + Path finalImage = createJavaImageRuntimeLink(new BaseJlinkSpecBuilder() .addExtraOption("--add-options") .addExtraOption("-Xlog:gc=info:stderr -XX:+UseParallelGC") .name("java-base-with-opts") diff --git a/test/jdk/tools/jlink/JmodLess/BasicJlinkTest.java b/test/jdk/tools/jlink/JmodLess/BasicJlinkTest.java index 1784eb0f806d8..b39dcd0f5a9a6 100644 --- a/test/jdk/tools/jlink/JmodLess/BasicJlinkTest.java +++ b/test/jdk/tools/jlink/JmodLess/BasicJlinkTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -29,8 +29,8 @@ /* * @test - * @summary Test basic jmod-less jlinking - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @summary Test basic runtime-image-based jlinking + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -42,21 +42,21 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g BasicJlinkTest */ -public class BasicJlinkTest extends AbstractJmodLessTest { +public class BasicJlinkTest extends AbstractLinkableRuntimeTest { @Override public void runTest(Helper helper) throws Exception { - Path finalImage = createJavaBaseJmodLess(helper, "java-base"); + Path finalImage = createJavaBaseRuntimeLink(helper, "java-base"); verifyListModules(finalImage, List.of("java.base")); } - private Path createJavaBaseJmodLess(Helper helper, String name) throws Exception { + private Path createJavaBaseRuntimeLink(Helper helper, String name) throws Exception { BaseJlinkSpecBuilder builder = new BaseJlinkSpecBuilder(); builder.helper(helper) .name(name) .addModule("java.base") .validatingModule("java.base"); - return createJavaImageJmodLess(builder.build()); + return createJavaImageRuntimeLink(builder.build()); } public static void main(String[] args) throws Exception { diff --git a/test/jdk/tools/jlink/JmodLess/CapturingHandler.java b/test/jdk/tools/jlink/JmodLess/CapturingHandler.java index e754fec8583b9..8902cd75198ef 100644 --- a/test/jdk/tools/jlink/JmodLess/CapturingHandler.java +++ b/test/jdk/tools/jlink/JmodLess/CapturingHandler.java @@ -23,7 +23,7 @@ import jdk.test.lib.process.OutputAnalyzer; -class CapturingHandler extends AbstractJmodLessTest.OutputAnalyzerHandler { +class CapturingHandler extends AbstractLinkableRuntimeTest.OutputAnalyzerHandler { private OutputAnalyzer output; diff --git a/test/jdk/tools/jlink/JmodLess/CustomModuleJlinkTest.java b/test/jdk/tools/jlink/JmodLess/CustomModuleJlinkTest.java index 6c9d5e057bee0..ddb9bf7f20046 100644 --- a/test/jdk/tools/jlink/JmodLess/CustomModuleJlinkTest.java +++ b/test/jdk/tools/jlink/JmodLess/CustomModuleJlinkTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -30,7 +30,7 @@ /* * @test * @summary Test jmod-less jlink with a custom module - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -42,7 +42,7 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g CustomModuleJlinkTest */ -public class CustomModuleJlinkTest extends AbstractJmodLessTest { +public class CustomModuleJlinkTest extends AbstractLinkableRuntimeTest { public static void main(String[] args) throws Exception { CustomModuleJlinkTest test = new CustomModuleJlinkTest(); @@ -54,22 +54,21 @@ void runTest(Helper helper) throws Exception { String customModule = "leaf1"; helper.generateDefaultJModule(customModule); - // create a base image including jdk.jlink and the leaf1 module. This will - // add the leaf1 module's module path. - Path jlinkImage = createBaseJlinkImage(new BaseJlinkSpecBuilder() + // create a base image for runtime linking + Path jlinkImage = createRuntimeLinkImage(new BaseJlinkSpecBuilder() .helper(helper) .name("cmod-jlink") - .addModule(customModule) - .validatingModule("java.base") // not used + .addModule("java.base") + .validatingModule("java.base") .build()); - // Now that the base image already includes the 'leaf1' module, it should - // be possible to jlink it again, asking for *only* the 'leaf1' plugin even - // though we won't have any jmods directories present. + // Next jlink using the current runtime 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) diff --git a/test/jdk/tools/jlink/JmodLess/GenerateJLIClassesTest.java b/test/jdk/tools/jlink/JmodLess/GenerateJLIClassesTest.java index 1ac1381d7b4c3..379f2c979f020 100644 --- a/test/jdk/tools/jlink/JmodLess/GenerateJLIClassesTest.java +++ b/test/jdk/tools/jlink/JmodLess/GenerateJLIClassesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -30,7 +30,7 @@ /* * @test * @summary Verify JLI class generation in run-time image link mode - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -42,7 +42,7 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g GenerateJLIClassesTest */ -public class GenerateJLIClassesTest extends AbstractJmodLessTest { +public class GenerateJLIClassesTest extends AbstractLinkableRuntimeTest { public static void main(String[] args) throws Exception { GenerateJLIClassesTest test = new GenerateJLIClassesTest(); @@ -57,27 +57,20 @@ public static void main(String[] args) throws Exception { */ @Override void runTest(Helper helper) 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 - 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)); - Path jmodLessImage = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + Path runtimeLinkableImage = createRuntimeLinkImage(new BaseJlinkSpecBuilder() .helper(helper) .name("jlink.jli-jmodless") - .addModule("jdk.jlink") .validatingModule("java.base") - .addExtraOption("--exclude-resources") - .addExtraOption(EXCLUDE_RESOURCE_GLOB_STAMP) .build()); // Finally attempt another jmodless link reducing modules to java.base only, // and asking for specific jli classes. jlinkUsingImage(new JlinkSpecBuilder() .helper(helper) - .imagePath(jmodLessImage) + .imagePath(runtimeLinkableImage) .name("java.base-jli-derived") .addModule("java.base") .extraJlinkOpt("--generate-jli-classes=@" + baseFile.toString()) diff --git a/test/jdk/tools/jlink/JmodLess/JavaSEReproducibleTest.java b/test/jdk/tools/jlink/JmodLess/JavaSEReproducibleTest.java index 0e14a0ac50e28..06cb90b0be103 100644 --- a/test/jdk/tools/jlink/JmodLess/JavaSEReproducibleTest.java +++ b/test/jdk/tools/jlink/JmodLess/JavaSEReproducibleTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -28,8 +28,8 @@ /* * @test - * @summary Test reproducibility of jmod-less jlink of java.se - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @summary Test reproducibility of runtime image based jlink of java.se + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -41,7 +41,7 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g JavaSEReproducibleTest */ -public class JavaSEReproducibleTest extends AbstractJmodLessTest { +public class JavaSEReproducibleTest extends AbstractLinkableRuntimeTest { public static void main(String[] args) throws Exception { JavaSEReproducibleTest test = new JavaSEReproducibleTest(); @@ -52,7 +52,7 @@ public static void main(String[] args) throws Exception { void runTest(Helper helper) throws Exception { String javaSeModule = "java.se"; // create a java.se using jmod-less approach - Path javaSEJmodLess1 = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + Path javaSEJmodLess1 = createJavaImageRuntimeLink(new BaseJlinkSpecBuilder() .helper(helper) .name("java-se-repro1") .addModule(javaSeModule) @@ -60,7 +60,7 @@ void runTest(Helper helper) throws Exception { .build()); // create another java.se version using jmod-less approach - Path javaSEJmodLess2 = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + Path javaSEJmodLess2 = createJavaImageRuntimeLink(new BaseJlinkSpecBuilder() .helper(helper) .name("java-se-repro2") .addModule(javaSeModule) diff --git a/test/jdk/tools/jlink/JmodLess/ModifiedFilesExitTest.java b/test/jdk/tools/jlink/JmodLess/ModifiedFilesExitTest.java index 4eb22adc98841..24466e38bd4a4 100644 --- a/test/jdk/tools/jlink/JmodLess/ModifiedFilesExitTest.java +++ b/test/jdk/tools/jlink/JmodLess/ModifiedFilesExitTest.java @@ -1,3 +1,26 @@ +/* + * 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; @@ -6,8 +29,9 @@ /* * @test - * @summary Verify jlink fails by default when jlinking in jmod-less mode and files have been changed - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @summary Verify jlink fails by default when jlinking in runtime-image-based mode + * and files have been changed + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile diff --git a/test/jdk/tools/jlink/JmodLess/ModifiedFilesTest.java b/test/jdk/tools/jlink/JmodLess/ModifiedFilesTest.java index 293200c509fc5..18fe92655f103 100644 --- a/test/jdk/tools/jlink/JmodLess/ModifiedFilesTest.java +++ b/test/jdk/tools/jlink/JmodLess/ModifiedFilesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -30,22 +30,18 @@ import tests.Helper; -public abstract class ModifiedFilesTest extends AbstractJmodLessTest { +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) throws Exception { - Path initialImage = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + Path initialImage = createRuntimeLinkImage(new BaseJlinkSpecBuilder() //.name("java-base-jlink-with-mod") .name(initialImageName()) .addModule("java.base") - .addModule("jdk.jlink") .validatingModule("java.base") - // avoid producing the runtime image stamp file - .addExtraOption("--exclude-resources") - .addExtraOption(EXCLUDE_RESOURCE_GLOB_STAMP) .helper(helper) .build()); diff --git a/test/jdk/tools/jlink/JmodLess/ModifiedFilesWarningTest.java b/test/jdk/tools/jlink/JmodLess/ModifiedFilesWarningTest.java index c68d28240bc2e..48e3d55915e38 100644 --- a/test/jdk/tools/jlink/JmodLess/ModifiedFilesWarningTest.java +++ b/test/jdk/tools/jlink/JmodLess/ModifiedFilesWarningTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -28,8 +28,9 @@ /* * @test - * @summary Verify warnings are being produced when jlinking in jmod-less mode and files have been changed - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @summary Verify warnings are being produced when jlinking in runtime-based image + * mode and files have been modified + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile diff --git a/test/jdk/tools/jlink/JmodLess/MultiHopTest.java b/test/jdk/tools/jlink/JmodLess/MultiHopTest.java index e41a2890b0f2b..c67b858485e62 100644 --- a/test/jdk/tools/jlink/JmodLess/MultiHopTest.java +++ b/test/jdk/tools/jlink/JmodLess/MultiHopTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -29,8 +29,8 @@ /* * @test - * @summary Verify that a jlink unsing the run-image only is single-hop only - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @summary Verify that a jlink using the run-time image cannot include jdk.jlink + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -42,7 +42,7 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g MultiHopTest */ -public class MultiHopTest extends AbstractJmodLessTest { +public class MultiHopTest extends AbstractLinkableRuntimeTest { @Override void runTest(Helper helper) throws Exception { @@ -59,15 +59,14 @@ public boolean test(OutputAnalyzer a) { .helper(helper) .imagePath(jdkJlinkJmodless) .name("jdk-jlink-multi-hop1-target") - .addModule("java.base") + .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 multi-hop (hop 2)"); + throw new AssertionError("Expected jlink to fail due to including jdk.jlink"); } - String expectedMsg = "Module path to the JDK packaged modules must be specified. " + - "Run-time image based linking is not supported as $java.home was already created from a run-time image."; + String expectedMsg = "Including jdk.jlink module for run-time image based links is not allowed."; analyzer.stdoutShouldContain(expectedMsg); analyzer.stdoutShouldNotContain("Exception"); // ensure error message is sane } @@ -78,7 +77,7 @@ private Path createJDKJlinkJmodLess(Helper helper, String name) throws Exception .name(name) .addModule("jdk.jlink") .validatingModule("java.base"); - return createJavaImageJmodLess(builder.build()); + return createRuntimeLinkImage(builder.build()); } public static void main(String[] args) throws Exception { diff --git a/test/jdk/tools/jlink/JmodLess/PackagedModulesVsJmodLessTest.java b/test/jdk/tools/jlink/JmodLess/PackagedModulesVsJmodLessTest.java index 22bb1e0f773c6..c84b929e55917 100644 --- a/test/jdk/tools/jlink/JmodLess/PackagedModulesVsJmodLessTest.java +++ b/test/jdk/tools/jlink/JmodLess/PackagedModulesVsJmodLessTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -38,8 +38,9 @@ /* * @test - * @summary Compare packaged-modules jlink with a jmod-less jlink to produce the same result - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @summary Compare packaged-modules jlink with a run-time image based jlink to + * produce the same result + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -51,7 +52,7 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g PackagedModulesVsJmodLessTest */ -public class PackagedModulesVsJmodLessTest extends AbstractJmodLessTest { +public class PackagedModulesVsJmodLessTest extends AbstractLinkableRuntimeTest { public static void main(String[] args) throws Exception { PackagedModulesVsJmodLessTest test = new PackagedModulesVsJmodLessTest(); @@ -61,13 +62,11 @@ public static void main(String[] args) throws Exception { @Override void runTest(Helper helper) throws Exception { // create a java.se using jmod-less approach - Path javaSEJmodLess = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + Path javaSEruntimeLink = createJavaImageRuntimeLink(new BaseJlinkSpecBuilder() .helper(helper) .name("java-se-jmodless") .addModule("java.se") .validatingModule("java.se") - .addExtraOption("--exclude-resources") - .addExtraOption(EXCLUDE_RESOURCE_GLOB_STAMP) .build()); // create a java.se using packaged modules (jmod-full) @@ -75,7 +74,7 @@ void runTest(Helper helper) throws Exception { .output(helper.createNewImageDir("java-se-jmodfull")) .addMods("java.se").call().assertSuccess(); - compareRecursively(javaSEJmodLess, javaSEJmodFull); + compareRecursively(javaSEruntimeLink, javaSEJmodFull); } // Visit all files in the given directories checking that they're byte-by-byte identical diff --git a/test/jdk/tools/jlink/JmodLess/SaveJlinkOptsTest.java b/test/jdk/tools/jlink/JmodLess/SaveJlinkOptsTest.java deleted file mode 100644 index 86c0dddcaa29c..0000000000000 --- a/test/jdk/tools/jlink/JmodLess/SaveJlinkOptsTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.List; -import java.util.stream.Collectors; - -import jdk.test.lib.process.OutputAnalyzer; -import tests.Helper; - -/* - * @test - * @summary Test --save-jlink-argfiles plugin in jmod-less mode - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) - * @library ../../lib /test/lib - * @enablePreview - * @modules java.base/jdk.internal.classfile - * 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 SaveJlinkOptsTest - */ -public class SaveJlinkOptsTest extends AbstractJmodLessTest { - - public static void main(String[] args) throws Exception { - SaveJlinkOptsTest test = new SaveJlinkOptsTest(); - test.run(); - } - - @Override - void runTest(Helper helper) throws Exception { - String vendorVersion = "jmodless"; - Path jlinkOptsFile = createJlinkOptsFile(List.of("--compress", "zip-6", "--vendor-version", vendorVersion)); - Path finalImage = createJavaImageJmodLess(new BaseJlinkSpecBuilder() - .addExtraOption("--save-jlink-argfiles") - .addExtraOption(jlinkOptsFile.toAbsolutePath().toString()) - .addModule("jdk.jlink") - .name("java-base-with-jlink-opts") - .helper(helper) - .validatingModule("java.base") - .build()); - verifyVendorVersion(finalImage, vendorVersion); - } - - /** - * Create a temporary file for use via --save-jlink-argfiles-file - * @param options The options to save in the file. - * @return The path to the temporary file - */ - private static Path createJlinkOptsFile(List options) throws Exception { - Path tmpFile = Files.createTempFile("JLinkTestJmodsLess", "jlink-options-file"); - tmpFile.toFile().deleteOnExit(); - String content = options.stream().collect(Collectors.joining("\n")); - Files.writeString(tmpFile, content, StandardOpenOption.TRUNCATE_EXISTING); - return tmpFile; - } - - private void verifyVendorVersion(Path finalImage, String vendorVersion) throws Exception { - OutputAnalyzer out = runJavaCmd(finalImage, List.of("--version")); - String stdOut = out.getStdout(); - if (!stdOut.contains(vendorVersion)) { - if (DEBUG) { - System.err.println(stdOut); - } - throw new AssertionError("Expected vendor version '" + vendorVersion + "' in jlinked image."); - } - } - -} diff --git a/test/jdk/tools/jlink/JmodLess/SystemModulesTest.java b/test/jdk/tools/jlink/JmodLess/SystemModulesTest.java index 4cd548237ccfb..364aba5e92c1a 100644 --- a/test/jdk/tools/jlink/JmodLess/SystemModulesTest.java +++ b/test/jdk/tools/jlink/JmodLess/SystemModulesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -31,7 +31,7 @@ /* * @test * @summary Test appropriate handling of generated SystemModules* classes in run-time image link mode - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -43,7 +43,7 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g SystemModulesTest */ -public class SystemModulesTest extends AbstractJmodLessTest { +public class SystemModulesTest extends AbstractLinkableRuntimeTest { public static void main(String[] args) throws Exception { SystemModulesTest test = new SystemModulesTest(); @@ -60,34 +60,19 @@ void runTest(Helper helper) 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 - Path javaseJmodless = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + Path javaseJmodless = createJavaImageRuntimeLink(new BaseJlinkSpecBuilder() .helper(helper) .name("httpserver-jlink-jmodless-derived") .addModule("jdk.httpserver") .addModule("jdk.jdwp.agent") - .addModule("jdk.jlink") .validatingModule("java.base") - .addExtraOption("--exclude-resources") - .addExtraOption(EXCLUDE_RESOURCE_GLOB_STAMP) .build()); - // Verify that SystemModules$0.class etc. are there + // 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"), Collections.emptyList()); - // Finally attempt another jmodless link reducing modules to java.base only, - // no longer expecting SystemModules$0.class - jlinkUsingImage(new JlinkSpecBuilder() - .helper(helper) - .imagePath(javaseJmodless) - .name("java.base-from-jdk-httpserver-derived") - .addModule("java.base") - .expectedLocation("/java.base/jdk/internal/module/SystemModulesMap.class") - .expectedLocation("/java.base/jdk/internal/module/SystemModules.class") - .expectedLocation("/java.base/jdk/internal/module/SystemModules$all.class") - .unexpectedLocation("/java.base/jdk/internal/module/SystemModules$0.class") - .unexpectedLocation("/java.base/jdk/internal/module/SystemModules$default.class") - .validatingModule("java.base") - .build()); + "/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/JmodLess/SystemModulesTest2.java b/test/jdk/tools/jlink/JmodLess/SystemModulesTest2.java index d7d7a2106fdfe..3b7a740494ddb 100644 --- a/test/jdk/tools/jlink/JmodLess/SystemModulesTest2.java +++ b/test/jdk/tools/jlink/JmodLess/SystemModulesTest2.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Red Hat, Inc. + * 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 @@ -24,9 +24,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; -import java.util.function.Predicate; -import jdk.test.lib.process.OutputAnalyzer; import tests.Helper; import tests.JImageValidator; @@ -34,7 +32,7 @@ * @test * @summary Test SystemModules handling of java --list-modules with system modules * not consistently enabled/disabled. - * @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g) + * @requires (jlink.runtime.linkable & vm.compMode != "Xcomp" & os.maxMemory >= 2g) * @library ../../lib /test/lib * @enablePreview * @modules java.base/jdk.internal.classfile @@ -46,68 +44,31 @@ * jdk.test.lib.process.ProcessTools * @run main/othervm -Xmx1g SystemModulesTest2 */ -public class SystemModulesTest2 extends AbstractJmodLessTest { +public class SystemModulesTest2 extends AbstractLinkableRuntimeTest { public static void main(String[] args) throws Exception { SystemModulesTest2 test = new SystemModulesTest2(); test.run(); } - /* - * SystemModule classes are module specific and SystemModulesMap gets generated - * for each link. This turns out to be a problem if we perform an initial - * jmod-full link with system-modules plugin enabled, which in turn would - * change the SystemModulesMap in the run time image that the final run-time - * based link will then use to generate a link only using java.base. In that - * case, we cannot use the fast path and we ought to use the slow path in - * order to avoid CNFEs. - */ @Override void runTest(Helper helper) throws Exception { - // Create an image with two modules, so that SystemModulesMap gets generated - // for it as system-modules plugin is auto-enabled. Later, reduce the set - // of modules to only java.base with system-modules plugin disabled. - Path javaJmodless = createJavaImageJmodLess(new BaseJlinkSpecBuilder() + // See SystemModulesTest which enables the system-modules plugin. With + // it disabled, we expect for the generated classes to not be there. + Path javaJmodless = createJavaImageRuntimeLink(new BaseJlinkSpecBuilder() .helper(helper) .name("jlink-jmodless-sysmod2") .addModule("jdk.httpserver") - .addModule("jdk.jlink") .validatingModule("java.base") - .addExtraOption("--exclude-resources") - .addExtraOption(EXCLUDE_RESOURCE_GLOB_STAMP) + .addExtraOption("--disable-plugin") + .addExtraOption("system-modules") .build()); // Verify that SystemModules$all.class is there JImageValidator.validate(javaJmodless.resolve("lib").resolve("modules"), - List.of("/java.base/jdk/internal/module/SystemModules$all.class"), Collections.emptyList()); - // The following jlink using --disable-plugin system-modules ought to - // fail, since the SystemModulesMap class would be incorrect if not - // re-generated (due to disabled system-modules plugin). - 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(javaJmodless) - .name("java.base-from-sysmod2-derived") - .addModule("java.base") - .extraJlinkOpt("--disable-plugin") - .extraJlinkOpt("system-modules") - .validatingModule("java.base") - .build(), handler, exitFailPred); - OutputAnalyzer analyzer = handler.analyzer(); - if (analyzer.getExitValue() == 0) { - throw new AssertionError("Expected jlink to fail due to disable system-modules plugin!"); - } - analyzer.stdoutShouldContain("Disabling system-modules plugin for a run-time image based link is not allowed."); - // Verify the error message is reasonable - analyzer.stdoutShouldNotContain("jdk.tools.jlink.internal.RunImageLinkException"); - analyzer.stdoutShouldNotContain("java.lang.IllegalArgumentException"); - analyzer.stdoutShouldNotContain("java.lang.IllegalStateException"); + 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/jtreg-ext/requires/VMProps.java b/test/jtreg-ext/requires/VMProps.java index 7c5bfd6ab75b4..f3dfbd0cce0e6 100644 --- a/test/jtreg-ext/requires/VMProps.java +++ b/test/jtreg-ext/requires/VMProps.java @@ -23,6 +23,9 @@ package requires; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReader; +import java.lang.module.ModuleReference; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; @@ -39,6 +42,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import java.util.Set; import java.util.concurrent.Callable; @@ -140,6 +144,7 @@ public Map call() { map.put("jdk.containerized", this::jdkContainerized); map.put("vm.flagless", this::isFlagless); map.put("jdk.foreign.linker", this::jdkForeignLinker); + map.put("jlink.runtime.linkable", this::runtimeLinkable); map.putAll(xOptFlags()); // -Xmx4g -> @requires vm.opt.x.Xmx == "4g" ) vmGC(map); // vm.gc.X = true/false vmGCforCDS(map); // may set vm.gc @@ -654,6 +659,31 @@ private String jdkContainerized() { return "" + "true".equalsIgnoreCase(isEnabled); } + private String runtimeLinkable() { + // jdk.jlink module has the following resource listing native libs + // belonging to the java.base module for runtime linkable jimages. + String linkableRuntimeResource = "jdk/tools/jlink/internal/fs_java.base_files"; + try { + ModuleFinder finder = ModuleFinder.ofSystem(); + Optional ref = finder.find("jdk.jlink"); + if (ref.isEmpty()) { + // No jdk.jlink in the current image + return Boolean.FALSE.toString(); + } + try (ModuleReader reader = ref.get().open()) { + Optional inOpt = reader.open(linkableRuntimeResource); + if (inOpt.isPresent()) { + inOpt.get().close(); + 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".