# HG changeset patch # User sdrach # Date 1471047631 25200 # Fri Aug 12 17:20:31 2016 -0700 # Node ID 61381825d0ec9c3731c2e39ee97e9e26d5f26dc9 # Parent 011f836494527eb9271ac5c3144b95e93467acfa 8156499: Update jlink to support creating images with modules that are packaged as multi-release JARs Reviewed-by: Contributed-by: steve.drach@oracle.com diff --git a/src/java.base/share/classes/java/util/jar/JarFile.java b/src/java.base/share/classes/java/util/jar/JarFile.java --- a/src/java.base/share/classes/java/util/jar/JarFile.java +++ b/src/java.base/share/classes/java/util/jar/JarFile.java @@ -353,7 +353,7 @@ if (isMultiRelease) { return true; } - if (MULTI_RELEASE_ENABLED && versionMajor != BASE_VERSION_MAJOR) { + if (MULTI_RELEASE_ENABLED) { try { checkForSpecialAttributes(); } catch (IOException io) { @@ -960,7 +960,7 @@ hasClassPathAttribute = match(CLASSPATH_CHARS, b, CLASSPATH_LASTOCC) != -1; // is this a multi-release jar file - if (MULTI_RELEASE_ENABLED && versionMajor != BASE_VERSION_MAJOR) { + if (MULTI_RELEASE_ENABLED) { int i = match(MULTIRELEASE_CHARS, b, MULTIRELEASE_LASTOCC); if (i != -1) { i += MULTIRELEASE_CHARS.length; diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JarArchive.java @@ -29,10 +29,17 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.zip.ZipEntry; import java.util.zip.ZipFile; + import jdk.tools.jlink.internal.Archive.Entry.EntryType; /** @@ -43,13 +50,13 @@ /** * An entry located in a jar file. */ - private class JarEntry extends Entry { + private class JarFileEntry extends Entry { private final long size; - private final ZipEntry entry; - private final ZipFile file; + private final JarEntry entry; + private final JarFile file; - JarEntry(String path, String name, EntryType type, ZipFile file, ZipEntry entry) { + JarFileEntry(String path, String name, EntryType type, JarFile file, JarEntry entry) { super(JarArchive.this, path, name, type); this.entry = Objects.requireNonNull(entry); this.file = Objects.requireNonNull(file); @@ -74,8 +81,8 @@ private final Path file; private final String moduleName; - // currently processed ZipFile - private ZipFile zipFile; + // currently processed JarFile + private JarFile jarFile; protected JarArchive(String mn, Path file) { Objects.requireNonNull(mn); @@ -97,24 +104,24 @@ @Override public Stream entries() { try { - if (zipFile == null) { + if (jarFile == null) { open(); } } catch (IOException ioe) { throw new UncheckedIOException(ioe); } - return zipFile.stream().map(this::toEntry).filter(n -> n != null); + return versionedStream(jarFile).map(this::toEntry).filter(n -> n != null); } abstract EntryType toEntryType(String entryName); abstract String getFileName(String entryName); - private Entry toEntry(ZipEntry ze) { - String name = ze.getName(); + private Entry toEntry(JarEntry je) { + String name = je.getName(); String fn = getFileName(name); - if (ze.isDirectory() || fn.startsWith("_")) { + if (je.isDirectory() || fn.startsWith("_")) { return null; } @@ -123,21 +130,94 @@ if (fn.equals(MODULE_INFO)) { fn = moduleName + "/" + MODULE_INFO; } - return new JarEntry(ze.getName(), fn, rt, zipFile, ze); + return new JarFileEntry(je.getName(), fn, rt, jarFile, je); + } + + private Stream versionedStream(JarFile jf) { + if (!jf.isMultiRelease()) { + return jf.stream(); + } + + class Name { + private static final String VERSIONS_DIR = "META-INF/versions/"; + private static final int VERSIONS_DIR_LEN = 18; // VERSIONS_DIR.length(); + private final int version; + private final String name; + + Name(JarEntry je) { + String name = je.getName(); + if (name.startsWith(VERSIONS_DIR)) { + name = name.substring(VERSIONS_DIR_LEN); + int offset = name.indexOf('/'); + if (offset == -1) + throw new RuntimeException("Can not obtain version in multi-release jar"); + this.version = Integer.parseInt(name.substring(0, offset)); + this.name = name.substring(offset + 1); + } else { + this.name = name; + this.version = JarFile.baseVersion().major(); + } + } + + public String toString() { + return name; + } + } + + int version = jf.getVersion().major(); + int base = JarFile.baseVersion().major(); + Set finalNames = new HashSet<>(); + + Map> versionsMap = jf + .stream() + .filter(je -> !je.isDirectory()) + .filter(je -> !je.getName().equals("META-INF/MANIFEST.MF")) + .map(je -> new Name(je)) + .filter(nm -> nm.version <= version) + .collect(Collectors.groupingBy(name -> name.version)); + + // a legal multi-release jar always has a base entry + Set baseNames = versionsMap.get(base).stream() + .map(nm -> nm.name) + .collect(Collectors.toSet()); + + versionsMap.remove(base); + + finalNames.addAll(baseNames); + + versionsMap.keySet().forEach(v -> { + Stream names = versionsMap.get(v).stream().map(nm -> nm.name); + if (v == version) { + finalNames.addAll(names.collect(Collectors.toSet())); + } else { + finalNames.addAll( + names.filter(nm -> { + if (nm.endsWith(".class")) { + return baseNames.contains(nm); + } else { + // resource entries are "promoted" + return true; + } + }).collect(Collectors.toSet()) + ); + } + }); + + return finalNames.stream().map(nm -> jf.getJarEntry(nm)); } @Override public void close() throws IOException { - if (zipFile != null) { - zipFile.close(); + if (jarFile != null) { + jarFile.close(); } } @Override public void open() throws IOException { - if (zipFile != null) { - zipFile.close(); + if (jarFile != null) { + jarFile.close(); } - zipFile = new ZipFile(file.toFile()); + jarFile = new JarFile(file.toFile(), true, ZipFile.OPEN_READ, JarFile.runtimeVersion()); } } diff --git a/test/tools/jlink/multireleasejar/JLinkMultiReleaseJarTest.java b/test/tools/jlink/multireleasejar/JLinkMultiReleaseJarTest.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/JLinkMultiReleaseJarTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * 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. + */ + +/* + * @test + * @bug 8156499 + * @summary Test image creation from Multi-Release JAR + * @author Steve Drach + * @library /lib/testlibrary + * @modules java.base/jdk.internal.jimage + * jdk.jartool/sun.tools.jar + * jdk.jlink/jdk.tools.jlink.internal + * @build CompilerUtils + * @run testng JLinkMultiReleaseJarTest +*/ + +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jdk.internal.jimage.BasicImageReader; + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class JLinkMultiReleaseJarTest { + private Path userdir; + private Path javahome; + + @BeforeClass + public void initialize() throws IOException { + Path srcdir = Paths.get(System.getProperty("test.src")); + userdir = Paths.get(System.getProperty("user.dir", ".")); + javahome = Paths.get(System.getProperty("java.home")); + + // create class files from source + Path base = srcdir.resolve("base"); + Path basemods = userdir.resolve("basemods"); + CompilerUtils.compile(base, basemods, "-modulesourcepath", base.toString()); + + Path rt = srcdir.resolve("rt"); + Path rtmods = userdir.resolve("rtmods"); + CompilerUtils.compile(rt, rtmods, "-modulesourcepath", rt.toString()); + + // copy resource into basemods + Path resource = base.resolve("m1").resolve("resources.txt"); + Path dest = basemods.resolve("m1").resolve("resources.txt"); + Files.copy(resource, dest); + + // build multi-release jar file + sun.tools.jar.Main jartool = new sun.tools.jar.Main(System.out, System.err, "jar"); + String args = "-cf m1.jar -C basemods/m1 . --release " + + JarFile.runtimeVersion().major() + " -C rtmods/m1 ."; + jartool.run(args.split(" +")); + } + + //@AfterClass + public void close() throws IOException { + Files.walkFileTree(userdir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException + { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) + throws IOException + { + if (dir.equals(userdir)) { + return FileVisitResult.CONTINUE; + } + if (e == null) { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } else { + // directory iteration failed + throw e; + } + } + }); + } + + @Test + public void test() throws Throwable { + // check that there are only packaged modules (.jmod files) for modulepath + Path jmodsdir = javahome.resolve("jmods"); + try (Stream jmods = Files.walk(jmodsdir, 1)) { + if (!jmods + .filter(path -> !path.equals(jmodsdir)) + .filter(path -> !path.toString().endsWith(".jmod")) + .collect(Collectors.toSet()) + .isEmpty() + ) { + throw new Exception("Exploded modules found"); + } + } + + // use jlink to build image from multi-release jar + String args = "--output myimage --add-modules m1 --module-path m1.jar:" + + jmodsdir.toString(); + int exitCode = jdk.tools.jlink.internal.Main.run(args.split(" +"), + new PrintWriter(System.out)); + Assert.assertEquals(exitCode, 0); + + // validate image + Path jimage = userdir.resolve("myimage").resolve("lib").resolve("modules"); + BasicImageReader reader = BasicImageReader.open(jimage); + + // do we have the right entry names? + Set names = Arrays.stream(reader.getEntryNames()) + .filter(n -> n.startsWith("/m1")) + .collect(Collectors.toSet()); + Assert.assertEquals(names, Set.of( + "/m1/module-info.class", + "/m1/p/Main.class", + "/m1/p/Type.class", + "/m1/resources.txt")); + + // do we have the right code? + byte[] b = reader.getResource("/m1/p/Main.class"); + Class clazz = (new ByteArrayClassLoader()).loadClass("p.Main", b); + MethodHandle getVersion = MethodHandles.lookup() + .findVirtual(clazz, "getVersion", MethodType.methodType(int.class)); + int version = (int)getVersion.invoke(clazz.getConstructor().newInstance()); + Assert.assertEquals(version, JarFile.runtimeVersion().major()); + } + + private static class ByteArrayClassLoader extends ClassLoader { + public Class loadClass(String name, byte[] bytes) { + return defineClass(name, bytes, 0, bytes.length); + } + } +} diff --git a/test/tools/jlink/multireleasejar/base/m1/module-info.java b/test/tools/jlink/multireleasejar/base/m1/module-info.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/base/m1/module-info.java @@ -0,0 +1,3 @@ +module m1 { + exports p; +} diff --git a/test/tools/jlink/multireleasejar/base/m1/p/Main.java b/test/tools/jlink/multireleasejar/base/m1/p/Main.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/base/m1/p/Main.java @@ -0,0 +1,8 @@ +package p; + +public class Main { + private String msg = "something to give this a different size"; + public int getVersion() { + return 8; + } +} diff --git a/test/tools/jlink/multireleasejar/base/m1/resources.txt b/test/tools/jlink/multireleasejar/base/m1/resources.txt new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/base/m1/resources.txt @@ -0,0 +1,1 @@ +Sample resource file. diff --git a/test/tools/jlink/multireleasejar/rt/m1/module-info.java b/test/tools/jlink/multireleasejar/rt/m1/module-info.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/rt/m1/module-info.java @@ -0,0 +1,3 @@ +module m1 { + exports p; +} diff --git a/test/tools/jlink/multireleasejar/rt/m1/p/Main.java b/test/tools/jlink/multireleasejar/rt/m1/p/Main.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/rt/m1/p/Main.java @@ -0,0 +1,9 @@ +package p; + +import java.util.jar.JarFile; + +public class Main { + public int getVersion() { + return JarFile.runtimeVersion().major(); + } +} diff --git a/test/tools/jlink/multireleasejar/rt/m1/p/Type.java b/test/tools/jlink/multireleasejar/rt/m1/p/Type.java new file mode 100644 --- /dev/null +++ b/test/tools/jlink/multireleasejar/rt/m1/p/Type.java @@ -0,0 +1,3 @@ +package p; + +class Type{}