src/jdk.jartool/share/classes/sun/tools/jar/Main.java

Print this page
rev 14440 : 8156497: Add jar tool support for Multi-Release Modular JARs
Reviewed-by: alanb

*** 36,45 **** --- 36,46 ---- import java.lang.module.ModuleReader; import java.lang.module.ModuleReference; import java.lang.module.ResolutionException; import java.lang.module.ResolvedModule; import java.net.URI; + import java.nio.ByteBuffer; import java.nio.file.Path; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; import java.util.function.Consumer;
*** 60,69 **** --- 61,71 ---- import jdk.internal.util.jar.JarIndex; import static jdk.internal.util.jar.JarIndex.INDEX_NAME; import static java.util.jar.JarFile.MANIFEST_NAME; import static java.util.stream.Collectors.joining; + import static java.util.stream.Collectors.toSet; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; /** * This class implements a simple utility for creating files in the JAR * (Java Archive) file format. The JAR format is based on the ZIP file
*** 127,140 **** Pattern modulesToHash; ModuleFinder moduleFinder = ModuleFinder.empty(); private static final String MODULE_INFO = "module-info.class"; - Path moduleInfo; - private boolean isModularJar() { return moduleInfo != null; } - static final String MANIFEST_DIR = "META-INF/"; static final String VERSION = "1.0"; private static ResourceBundle rsrc; /** --- 129,140 ---- Pattern modulesToHash; ModuleFinder moduleFinder = ModuleFinder.empty(); private static final String MODULE_INFO = "module-info.class"; static final String MANIFEST_DIR = "META-INF/"; + static final String VERSIONS_DIR = MANIFEST_DIR + "versions/"; static final String VERSION = "1.0"; private static ResourceBundle rsrc; /**
*** 240,255 **** } if (ename != null) { addMainClass(manifest, ename); } } ! expand(null, files, false); - byte[] moduleInfoBytes = null; - if (isModularJar()) { - moduleInfoBytes = addExtendedModuleAttributes( - readModuleInfo(moduleInfo)); } else if (moduleVersion != null || modulesToHash != null) { error(getMsg("error.module.options.without.info")); return false; } --- 240,270 ---- } if (ename != null) { addMainClass(manifest, ename); } } ! Map<String,Path> moduleInfoPaths = new HashMap<>(); ! expand(null, files, false, moduleInfoPaths); ! ! Map<String,byte[]> moduleInfos = new LinkedHashMap<>(); ! if (!moduleInfoPaths.isEmpty()) { ! if (!checkModuleInfos(moduleInfoPaths)) ! return false; ! ! // root module-info first ! byte[] b = readModuleInfo(moduleInfoPaths.get(MODULE_INFO)); ! moduleInfos.put(MODULE_INFO, b); ! for (Map.Entry<String,Path> e : moduleInfoPaths.entrySet()) ! moduleInfos.putIfAbsent(e.getKey(), readModuleInfo(e.getValue())); ! ! if (!addExtendedModuleAttributes(moduleInfos)) ! return false; ! ! // Basic consistency checks for modular jars. ! if (!checkServices(moduleInfos.get(MODULE_INFO))) ! return false; } else if (moduleVersion != null || modulesToHash != null) { error(getMsg("error.module.options.without.info")); return false; }
*** 272,288 **** : fname.substring(fname.indexOf(File.separatorChar) + 1); if (nflag) { tmpfile = createTemporaryFile(tmpbase, ".jar"); out = new FileOutputStream(tmpfile); } ! create(new BufferedOutputStream(out, 4096), manifest, moduleInfoBytes); ! ! // Consistency checks for modular jars. ! if (isModularJar()) { ! if (!checkServices(moduleInfoBytes)) ! return false; ! } if (in != null) { in.close(); } out.close(); --- 287,297 ---- : fname.substring(fname.indexOf(File.separatorChar) + 1); if (nflag) { tmpfile = createTemporaryFile(tmpbase, ".jar"); out = new FileOutputStream(tmpfile); } ! create(new BufferedOutputStream(out, 4096), manifest, moduleInfos); if (in != null) { in.close(); } out.close();
*** 335,357 **** out = new FileOutputStream(FileDescriptor.out); vflag = false; } InputStream manifest = (!Mflag && (mname != null)) ? (new FileInputStream(mname)) : null; - expand(null, files, true); ! byte[] moduleInfoBytes = null; ! if (isModularJar()) { ! moduleInfoBytes = readModuleInfo(moduleInfo); ! } boolean updateOk = update(in, new BufferedOutputStream(out), ! manifest, moduleInfoBytes, null); // Consistency checks for modular jars. ! if (isModularJar()) { ! if(!checkServices(moduleInfoBytes)) return false; } if (ok) { ok = updateOk; --- 344,367 ---- out = new FileOutputStream(FileDescriptor.out); vflag = false; } InputStream manifest = (!Mflag && (mname != null)) ? (new FileInputStream(mname)) : null; ! Map<String,Path> moduleInfoPaths = new HashMap<>(); ! expand(null, files, true, moduleInfoPaths); ! ! Map<String,byte[]> moduleInfos = new HashMap<>(); ! for (Map.Entry<String,Path> e : moduleInfoPaths.entrySet()) ! moduleInfos.put(e.getKey(), readModuleInfo(e.getValue())); boolean updateOk = update(in, new BufferedOutputStream(out), ! manifest, moduleInfos, null); // Consistency checks for modular jars. ! if (!moduleInfos.isEmpty()) { ! if(!checkServices(moduleInfos.get(MODULE_INFO))) return false; } if (ok) { ok = updateOk;
*** 636,646 **** /** * Expands list of files to process into full list of all files that * can be found by recursively descending directories. */ ! void expand(File dir, String[] files, boolean isUpdate) throws IOException { if (files == null) return; for (int i = 0; i < files.length; i++) { File f; --- 646,661 ---- /** * Expands list of files to process into full list of all files that * can be found by recursively descending directories. */ ! void expand(File dir, ! String[] files, ! boolean isUpdate, ! Map<String,Path> moduleInfoPaths) ! throws IOException ! { if (files == null) return; for (int i = 0; i < files.length; i++) { File f;
*** 649,680 **** else f = new File(dir, files[i]); if (f.isFile()) { String path = f.getPath(); ! if (entryName(path).equals(MODULE_INFO)) { ! if (moduleInfo != null && vflag) ! output(formatMsg("error.unexpected.module-info", path)); ! moduleInfo = f.toPath(); if (isUpdate) ! entryMap.put(entryName(path), f); } else if (entries.add(f)) { ! jarEntries.add(entryName(path)); ! if (path.endsWith(".class")) ! packages.add(toPackageName(entryName(path))); if (isUpdate) ! entryMap.put(entryName(f.getPath()), f); } } else if (f.isDirectory()) { if (entries.add(f)) { if (isUpdate) { String dirPath = f.getPath(); dirPath = (dirPath.endsWith(File.separator)) ? dirPath : (dirPath + File.separator); entryMap.put(entryName(dirPath), f); } ! expand(f, f.list(), isUpdate); } } else { error(formatMsg("error.nosuch.fileordir", String.valueOf(f))); ok = false; } --- 664,694 ---- else f = new File(dir, files[i]); if (f.isFile()) { String path = f.getPath(); ! String entryName = entryName(path); ! if (entryName.endsWith(MODULE_INFO)) { ! moduleInfoPaths.put(entryName, f.toPath()); if (isUpdate) ! entryMap.put(entryName, f); } else if (entries.add(f)) { ! jarEntries.add(entryName); ! if (path.endsWith(".class") && !entryName.startsWith(VERSIONS_DIR)) ! packages.add(toPackageName(entryName)); if (isUpdate) ! entryMap.put(entryName, f); } } else if (f.isDirectory()) { if (entries.add(f)) { if (isUpdate) { String dirPath = f.getPath(); dirPath = (dirPath.endsWith(File.separator)) ? dirPath : (dirPath + File.separator); entryMap.put(entryName(dirPath), f); } ! expand(f, f.list(), isUpdate, moduleInfoPaths); } } else { error(formatMsg("error.nosuch.fileordir", String.valueOf(f))); ok = false; }
*** 682,692 **** } /** * Creates a new JAR file. */ ! void create(OutputStream out, Manifest manifest, byte[] moduleInfoBytes) throws IOException { ZipOutputStream zos = new JarOutputStream(out); if (flag0) { zos.setMethod(ZipOutputStream.STORED); --- 696,706 ---- } /** * Creates a new JAR file. */ ! void create(OutputStream out, Manifest manifest, Map<String,byte[]> moduleInfos) throws IOException { ZipOutputStream zos = new JarOutputStream(out); if (flag0) { zos.setMethod(ZipOutputStream.STORED);
*** 708,728 **** } zos.putNextEntry(e); manifest.write(zos); zos.closeEntry(); } ! if (moduleInfoBytes != null) { if (vflag) { ! output(getMsg("out.added.module-info")); } ! ZipEntry e = new ZipEntry(MODULE_INFO); e.setTime(System.currentTimeMillis()); if (flag0) { ! crc32ModuleInfo(e, moduleInfoBytes); } zos.putNextEntry(e); ! ByteArrayInputStream in = new ByteArrayInputStream(moduleInfoBytes); in.transferTo(zos); zos.closeEntry(); } for (File file: entries) { addFile(zos, file); --- 722,744 ---- } zos.putNextEntry(e); manifest.write(zos); zos.closeEntry(); } ! for (Map.Entry<String,byte[]> mi : moduleInfos.entrySet()) { ! String entryName = mi.getKey(); ! byte[] miBytes = mi.getValue(); if (vflag) { ! output(formatMsg("out.added.module-info", entryName)); } ! ZipEntry e = new ZipEntry(mi.getKey()); e.setTime(System.currentTimeMillis()); if (flag0) { ! crc32ModuleInfo(e, miBytes); } zos.putNextEntry(e); ! ByteArrayInputStream in = new ByteArrayInputStream(miBytes); in.transferTo(zos); zos.closeEntry(); } for (File file: entries) { addFile(zos, file);
*** 753,774 **** } return true; } /** * Updates an existing jar file. */ boolean update(InputStream in, OutputStream out, InputStream newManifest, ! byte[] newModuleInfoBytes, JarIndex jarIndex) throws IOException { ZipInputStream zis = new ZipInputStream(in); ZipOutputStream zos = new JarOutputStream(out); ZipEntry e = null; boolean foundManifest = false; - boolean foundModuleInfo = false; boolean updateOk = true; if (jarIndex != null) { addIndex(jarIndex, zos); } --- 769,813 ---- } return true; } /** + * Returns true of the given module-info's are located in acceptable + * locations. Otherwise, outputs an appropriate message and returns false. + */ + private boolean checkModuleInfos(Map<String,?> moduleInfos) { + // there must always be, at least, a root module-info + if (!moduleInfos.containsKey(MODULE_INFO)) { + error(getMsg("error.versioned.info.without.root")); + return false; + } + + // module-info can only appear in the root, or a versioned section + Optional<String> other = moduleInfos.keySet().stream() + .filter(x -> !x.equals(MODULE_INFO)) + .filter(x -> !x.startsWith(VERSIONS_DIR)) + .findFirst(); + + if (other.isPresent()) { + error(formatMsg("error.unexpected.module-info", other.get())); + return false; + } + return true; + } + + /** * Updates an existing jar file. */ boolean update(InputStream in, OutputStream out, InputStream newManifest, ! Map<String,byte[]> moduleInfos, JarIndex jarIndex) throws IOException { ZipInputStream zis = new ZipInputStream(in); ZipOutputStream zos = new JarOutputStream(out); ZipEntry e = null; boolean foundManifest = false; boolean updateOk = true; if (jarIndex != null) { addIndex(jarIndex, zos); }
*** 776,786 **** // put the old entries first, replace if necessary while ((e = zis.getNextEntry()) != null) { String name = e.getName(); boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME); ! boolean isModuleInfoEntry = name.equals(MODULE_INFO); if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME)) || (Mflag && isManifestEntry)) { continue; } else if (isManifestEntry && ((newManifest != null) || --- 815,825 ---- // put the old entries first, replace if necessary while ((e = zis.getNextEntry()) != null) { String name = e.getName(); boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME); ! boolean isModuleInfoEntry = name.endsWith(MODULE_INFO); if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME)) || (Mflag && isManifestEntry)) { continue; } else if (isManifestEntry && ((newManifest != null) ||
*** 804,820 **** old.read(newManifest); } if (!updateManifest(old, zos)) { return false; } ! } else if (isModuleInfoEntry ! && ((newModuleInfoBytes != null) || (ename != null) ! || moduleVersion != null || modulesToHash != null)) { ! if (newModuleInfoBytes == null) { ! // Update existing module-info.class ! newModuleInfoBytes = readModuleInfo(zis); ! } } else { if (!entryMap.containsKey(name)) { // copy the old stuff // do our own compression ZipEntry e2 = new ZipEntry(name); e2.setMethod(e.getMethod()); --- 843,854 ---- old.read(newManifest); } if (!updateManifest(old, zos)) { return false; } ! } else if (moduleInfos != null && isModuleInfoEntry) { ! moduleInfos.putIfAbsent(name, readModuleInfo(zis)); } else { if (!entryMap.containsKey(name)) { // copy the old stuff // do our own compression ZipEntry e2 = new ZipEntry(name); e2.setMethod(e.getMethod());
*** 858,875 **** updateOk = false; } } } ! // write the module-info.class ! if (newModuleInfoBytes != null) { ! newModuleInfoBytes = addExtendedModuleAttributes(newModuleInfoBytes); // TODO: check manifest main classes, etc ! if (!updateModuleInfo(newModuleInfoBytes, zos)) { updateOk = false; } } else if (moduleVersion != null || modulesToHash != null) { error(getMsg("error.module.options.without.info")); updateOk = false; } --- 892,918 ---- updateOk = false; } } } ! if (moduleInfos != null && !moduleInfos.isEmpty()) { ! if (!checkModuleInfos(moduleInfos)) ! updateOk = false; ! ! if (updateOk) { ! if (!addExtendedModuleAttributes(moduleInfos)) ! updateOk = false; ! } // TODO: check manifest main classes, etc ! ! if (updateOk) { ! for (Map.Entry<String,byte[]> mi : moduleInfos.entrySet()) { ! if (!updateModuleInfo(mi.getValue(), zos, mi.getKey())) updateOk = false; } + } } else if (moduleVersion != null || modulesToHash != null) { error(getMsg("error.module.options.without.info")); updateOk = false; }
*** 892,913 **** zos.putNextEntry(e); index.write(zos); zos.closeEntry(); } ! private boolean updateModuleInfo(byte[] moduleInfoBytes, ZipOutputStream zos) throws IOException { ! ZipEntry e = new ZipEntry(MODULE_INFO); e.setTime(System.currentTimeMillis()); if (flag0) { crc32ModuleInfo(e, moduleInfoBytes); } zos.putNextEntry(e); zos.write(moduleInfoBytes); if (vflag) { ! output(getMsg("out.update.module-info")); } return true; } private boolean updateManifest(Manifest m, ZipOutputStream zos) --- 935,956 ---- zos.putNextEntry(e); index.write(zos); zos.closeEntry(); } ! private boolean updateModuleInfo(byte[] moduleInfoBytes, ZipOutputStream zos, String entryName) throws IOException { ! ZipEntry e = new ZipEntry(entryName); e.setTime(System.currentTimeMillis()); if (flag0) { crc32ModuleInfo(e, moduleInfoBytes); } zos.putNextEntry(e); zos.write(moduleInfoBytes); if (vflag) { ! output(formatMsg("out.update.module-info", entryName)); } return true; } private boolean updateManifest(Manifest m, ZipOutputStream zos)
*** 1708,1724 **** private static String toBinaryName(String classname) { return (classname.replace('.', '/')) + ".class"; } private boolean checkServices(byte[] moduleInfoBytes) throws IOException { ! ModuleDescriptor md; ! try (InputStream in = new ByteArrayInputStream(moduleInfoBytes)) { ! md = ModuleDescriptor.read(in); ! } Set<String> missing = md.provides() .values() .stream() .map(Provides::providers) .flatMap(Set::stream) --- 1751,1765 ---- private static String toBinaryName(String classname) { return (classname.replace('.', '/')) + ".class"; } + /* A module must have the implementation class of the services it 'provides'. */ private boolean checkServices(byte[] moduleInfoBytes) throws IOException { ! ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(moduleInfoBytes)); Set<String> missing = md.provides() .values() .stream() .map(Provides::providers) .flatMap(Set::stream)
*** 1730,1797 **** } return true; } /** ! * Returns a byte array containing the module-info.class. * * If --module-version, --main-class, or other options were provided * then the corresponding class file attributes are added to the * module-info here. */ ! private byte[] addExtendedModuleAttributes(byte[] moduleInfoBytes) throws IOException { ! assert isModularJar(); ! ! ModuleDescriptor md; ! try (InputStream in = new ByteArrayInputStream(moduleInfoBytes)) { ! md = ModuleDescriptor.read(in); ! } ! String name = md.name(); ! Set<String> exported = md.exports() ! .stream() ! .map(ModuleDescriptor.Exports::source) ! .collect(Collectors.toSet()); ! ! // copy the module-info.class into the jmod with the additional ! // attributes for the version, main class and other meta data ! try (InputStream in = new ByteArrayInputStream(moduleInfoBytes); ! ByteArrayOutputStream baos = new ByteArrayOutputStream()) { ! ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); // Add (or replace) the ConcealedPackages attribute - Set<String> conceals = packages.stream() - .filter(p -> !exported.contains(p)) - .collect(Collectors.toSet()); - extender.conceals(conceals); // --main-class if (ename != null) extender.mainClass(ename); // --module-version if (moduleVersion != null) extender.version(moduleVersion); // --hash-modules if (modulesToHash != null) { Hasher hasher = new Hasher(md, fname); ! ModuleHashes moduleHashes = hasher.computeHashes(name); if (moduleHashes != null) { extender.hashes(moduleHashes); } else { // should it issue warning or silent? ! System.out.println("warning: no module is recorded in hash in " + name); } } extender.write(baos); return baos.toByteArray(); } - } /** * Compute and record hashes */ private class Hasher { --- 1771,1915 ---- } return true; } /** ! * Adds extended modules attributes to the given module-info's. The given ! * Map values are updated in-place. Returns false if an error occurs. ! */ ! private boolean addExtendedModuleAttributes(Map<String,byte[]> moduleInfos) ! throws IOException ! { ! assert !moduleInfos.isEmpty() && moduleInfos.get(MODULE_INFO) != null; ! ! ByteBuffer bb = ByteBuffer.wrap(moduleInfos.get(MODULE_INFO)); ! ModuleDescriptor rd = ModuleDescriptor.read(bb); ! ! Set<String> exports = rd.exports() ! .stream() ! .map(Exports::source) ! .collect(toSet()); ! ! Set<String> conceals = packages.stream() ! .filter(p -> !exports.contains(p)) ! .collect(toSet()); ! ! for (Map.Entry<String,byte[]> e: moduleInfos.entrySet()) { ! ModuleDescriptor vd = ModuleDescriptor.read(ByteBuffer.wrap(e.getValue())); ! if (!(isValidVersionedDescriptor(vd, rd))) ! return false; ! e.setValue(extendedInfoBytes(rd, vd, e.getValue(), conceals)); ! } ! return true; ! } ! ! private static boolean isPlatformModule(String name) { ! return name.startsWith("java.") || name.startsWith("jdk."); ! } ! ! /** ! * Tells whether or not the given versioned module descriptor's attributes ! * are valid when compared against the given root module descriptor. ! * ! * A versioned module descriptor must be identical to the root module ! * descriptor, with two exceptions: ! * - A versioned descriptor can have different non-public `requires` ! * clauses of platform ( `java.*` and `jdk.*` ) modules, and ! * - A versioned descriptor can have different `uses` clauses, even of ! * service types defined outside of the platform modules. ! */ ! private boolean isValidVersionedDescriptor(ModuleDescriptor vd, ! ModuleDescriptor rd) ! throws IOException ! { ! if (!rd.name().equals(vd.name())) { ! fatalError(getMsg("error.versioned.info.name.notequal")); ! return false; ! } ! if (!rd.requires().equals(vd.requires())) { ! Set<Requires> rootRequires = rd.requires(); ! for (Requires r : vd.requires()) { ! if (rootRequires.contains(r)) { ! continue; ! } else if (r.modifiers().contains(Requires.Modifier.PUBLIC)) { ! fatalError(getMsg("error.versioned.info.requires.public")); ! return false; ! } else if (!isPlatformModule(r.name())) { ! fatalError(getMsg("error.versioned.info.requires.added")); ! return false; ! } ! } ! for (Requires r : rootRequires) { ! Set<Requires> mdRequires = vd.requires(); ! if (mdRequires.contains(r)) { ! continue; ! } else if (!isPlatformModule(r.name())) { ! fatalError(getMsg("error.versioned.info.requires.dropped")); ! return false; ! } ! } ! } ! if (!rd.exports().equals(vd.exports())) { ! fatalError(getMsg("error.versioned.info.exports.notequal")); ! return false; ! } ! if (!rd.provides().equals(vd.provides())) { ! fatalError(getMsg("error.versioned.info.provides.notequal")); ! return false; ! } ! return true; ! } ! ! /** ! * Returns a byte array containing the given module-info.class plus any ! * extended attributes. * * If --module-version, --main-class, or other options were provided * then the corresponding class file attributes are added to the * module-info here. */ ! private byte[] extendedInfoBytes(ModuleDescriptor rootDescriptor, ! ModuleDescriptor md, ! byte[] miBytes, ! Set<String> conceals) throws IOException { ! ByteArrayOutputStream baos = new ByteArrayOutputStream(); ! InputStream is = new ByteArrayInputStream(miBytes); ! ModuleInfoExtender extender = ModuleInfoExtender.newExtender(is); // Add (or replace) the ConcealedPackages attribute extender.conceals(conceals); // --main-class if (ename != null) extender.mainClass(ename); + else if (rootDescriptor.mainClass().isPresent()) + extender.mainClass(rootDescriptor.mainClass().get()); // --module-version if (moduleVersion != null) extender.version(moduleVersion); + else if (rootDescriptor.version().isPresent()) + extender.version(rootDescriptor.version().get()); // --hash-modules if (modulesToHash != null) { + String mn = md.name(); Hasher hasher = new Hasher(md, fname); ! ModuleHashes moduleHashes = hasher.computeHashes(mn); if (moduleHashes != null) { extender.hashes(moduleHashes); } else { // should it issue warning or silent? ! System.out.println("warning: no module is recorded in hash in " + mn); } } extender.write(baos); return baos.toByteArray(); } /** * Compute and record hashes */ private class Hasher {
*** 1863,1874 **** // find the modules that transitively depend upon the specified name Deque<String> deque = new ArrayDeque<>(); deque.add(name); Set<String> mods = visitNodes(graph, deque); ! // filter modules matching the pattern specified --hash-modules ! // as well as itself as the jmod file is being generated Map<String, Path> modulesForHash = mods.stream() .filter(mn -> !mn.equals(name) && modules.contains(mn)) .collect(Collectors.toMap(Function.identity(), moduleNameToPath::get)); if (modulesForHash.isEmpty()) --- 1981,1992 ---- // find the modules that transitively depend upon the specified name Deque<String> deque = new ArrayDeque<>(); deque.add(name); Set<String> mods = visitNodes(graph, deque); ! // filter modules matching the pattern specified in --hash-modules, ! // as well as the modular jar file that is being created / updated Map<String, Path> modulesForHash = mods.stream() .filter(mn -> !mn.equals(name) && modules.contains(mn)) .collect(Collectors.toMap(Function.identity(), moduleNameToPath::get)); if (modulesForHash.isEmpty())