jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java
changeset 36511 9d0388c6b336
parent 29914 48393dc87f88
child 36745 51effd3e92d0
--- a/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Tue Mar 15 13:48:26 2016 -0700
+++ b/jdk/src/jdk.jartool/share/classes/sun/tools/jar/Main.java	Thu Mar 17 19:04:16 2016 +0000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1996, 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 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
@@ -26,17 +26,35 @@
 package sun.tools.jar;
 
 import java.io.*;
+import java.lang.module.ModuleDescriptor;
+import java.lang.module.ModuleDescriptor.Exports;
+import java.lang.module.ModuleDescriptor.Provides;
+import java.lang.module.ModuleDescriptor.Requires;
+import java.lang.module.ModuleDescriptor.Version;
+import java.lang.module.ModuleFinder;
+import java.lang.module.ModuleReference;
+import java.lang.reflect.Method;
+import java.net.URI;
 import java.nio.file.Path;
 import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.*;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import java.util.zip.*;
 import java.util.jar.*;
 import java.util.jar.Pack200.*;
 import java.util.jar.Manifest;
 import java.text.MessageFormat;
+
+import jdk.internal.module.Hasher;
+import jdk.internal.module.ModuleInfoExtender;
 import sun.misc.JarIndex;
 import static sun.misc.JarIndex.INDEX_NAME;
 import static java.util.jar.JarFile.MANIFEST_NAME;
+import static java.util.stream.Collectors.joining;
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
 /**
@@ -60,6 +78,11 @@
 
     // All files need to be added/updated.
     Set<File> entries = new LinkedHashSet<File>();
+    // All packages.
+    Set<String> packages = new HashSet<>();
+    // All actual entries added, or existing, in the jar file ( excl manifest
+    // and module-info.class ). Populated during create or update.
+    Set<String> jarEntries = new HashSet<>();
 
     // Directories specified by "-C" operation.
     Set<String> paths = new HashSet<String>();
@@ -78,6 +101,30 @@
      */
     boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag;
 
+    /* To support additional GNU Style informational options */
+    enum Info {
+        HELP(GNUStyleOptions::printHelp),
+        COMPAT_HELP(GNUStyleOptions::printCompatHelp),
+        USAGE_SUMMARY(GNUStyleOptions::printUsageSummary),
+        VERSION(GNUStyleOptions::printVersion);
+
+        private Consumer<PrintStream> printFunction;
+        Info(Consumer<PrintStream> f) { this.printFunction = f; }
+        void print(PrintStream out) { printFunction.accept(out); }
+    };
+    Info info;
+
+    /* Modular jar related options */
+    boolean printModuleDescriptor;
+    Version moduleVersion;
+    Pattern dependenciesToHash;
+    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";
 
@@ -102,7 +149,7 @@
         }
     }
 
-    private String getMsg(String key) {
+    static String getMsg(String key) {
         try {
             return (rsrc.getString(key));
         } catch (MissingResourceException e) {
@@ -110,14 +157,14 @@
         }
     }
 
-    private String formatMsg(String key, String arg) {
+    static String formatMsg(String key, String arg) {
         String msg = getMsg(key);
         String[] args = new String[1];
         args[0] = arg;
         return MessageFormat.format(msg, (Object[]) args);
     }
 
-    private String formatMsg2(String key, String arg, String arg1) {
+    static String formatMsg2(String key, String arg, String arg1) {
         String msg = getMsg(key);
         String[] args = new String[2];
         args[0] = arg;
@@ -189,6 +236,16 @@
                     }
                 }
                 expand(null, files, false);
+
+                byte[] moduleInfoBytes = null;
+                if (isModularJar()) {
+                    moduleInfoBytes = addExtendedModuleAttributes(
+                            readModuleInfo(moduleInfo));
+                } else if (moduleVersion != null || dependenciesToHash != null) {
+                    error(getMsg("error.module.options.without.info"));
+                    return false;
+                }
+
                 OutputStream out;
                 if (fname != null) {
                     out = new FileOutputStream(fname);
@@ -210,7 +267,14 @@
                     tmpfile = createTemporaryFile(tmpbase, ".jar");
                     out = new FileOutputStream(tmpfile);
                 }
-                create(new BufferedOutputStream(out, 4096), manifest);
+                create(new BufferedOutputStream(out, 4096), manifest, moduleInfoBytes);
+
+                // Consistency checks for modular jars.
+                if (isModularJar()) {
+                    if (!checkServices(moduleInfoBytes))
+                        return false;
+                }
+
                 if (in != null) {
                     in.close();
                 }
@@ -267,8 +331,21 @@
                 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, null);
+                                          manifest, moduleInfoBytes, null);
+
+                // Consistency checks for modular jars.
+                if (isModularJar()) {
+                    if(!checkServices(moduleInfoBytes))
+                        return false;
+                }
+
                 if (ok) {
                     ok = updateOk;
                 }
@@ -328,6 +405,17 @@
                 }
             } else if (iflag) {
                 genIndex(rootjar, files);
+            } else if (printModuleDescriptor) {
+                boolean found;
+                if (fname != null) {
+                    found = printModuleDescriptor(new ZipFile(fname));
+                } else {
+                    try (FileInputStream fin = new FileInputStream(FileDescriptor.in)) {
+                        found = printModuleDescriptor(fin);
+                    }
+                }
+                if (!found)
+                    error(getMsg("error.module.descriptor.not.found"));
             }
         } catch (IOException e) {
             fatalError(e);
@@ -362,84 +450,111 @@
         int count = 1;
         try {
             String flags = args[0];
-            if (flags.startsWith("-")) {
-                flags = flags.substring(1);
-            }
-            for (int i = 0; i < flags.length(); i++) {
-                switch (flags.charAt(i)) {
-                case 'c':
-                    if (xflag || tflag || uflag || iflag) {
-                        usageError();
-                        return false;
-                    }
-                    cflag = true;
-                    break;
-                case 'u':
-                    if (cflag || xflag || tflag || iflag) {
-                        usageError();
-                        return false;
-                    }
-                    uflag = true;
-                    break;
-                case 'x':
-                    if (cflag || uflag || tflag || iflag) {
-                        usageError();
-                        return false;
-                    }
-                    xflag = true;
-                    break;
-                case 't':
-                    if (cflag || uflag || xflag || iflag) {
-                        usageError();
-                        return false;
+
+            // Note: flags.length == 2 can be treated as the short version of
+            // the GNU option since the there cannot be any other options,
+            // excluding -C, as per the old way.
+            if (flags.startsWith("--")
+                || (flags.startsWith("-") && flags.length() == 2)) {
+                try {
+                    count = GNUStyleOptions.parseOptions(this, args);
+                } catch (GNUStyleOptions.BadArgs x) {
+                    if (info != null) {
+                        info.print(out);
+                        return true;
                     }
-                    tflag = true;
-                    break;
-                case 'M':
-                    Mflag = true;
-                    break;
-                case 'v':
-                    vflag = true;
-                    break;
-                case 'f':
-                    fname = args[count++];
-                    break;
-                case 'm':
-                    mname = args[count++];
-                    break;
-                case '0':
-                    flag0 = true;
-                    break;
-                case 'i':
-                    if (cflag || uflag || xflag || tflag) {
-                        usageError();
-                        return false;
+                    error(x.getMessage());
+                    if (x.showUsage)
+                        Info.USAGE_SUMMARY.print(err);
+                    return false;
+                }
+            } else {
+                // Legacy/compatibility options
+                if (flags.startsWith("-")) {
+                    flags = flags.substring(1);
+                }
+                for (int i = 0; i < flags.length(); i++) {
+                    switch (flags.charAt(i)) {
+                        case 'c':
+                            if (xflag || tflag || uflag || iflag) {
+                                usageError();
+                                return false;
+                            }
+                            cflag = true;
+                            break;
+                        case 'u':
+                            if (cflag || xflag || tflag || iflag) {
+                                usageError();
+                                return false;
+                            }
+                            uflag = true;
+                            break;
+                        case 'x':
+                            if (cflag || uflag || tflag || iflag) {
+                                usageError();
+                                return false;
+                            }
+                            xflag = true;
+                            break;
+                        case 't':
+                            if (cflag || uflag || xflag || iflag) {
+                                usageError();
+                                return false;
+                            }
+                            tflag = true;
+                            break;
+                        case 'M':
+                            Mflag = true;
+                            break;
+                        case 'v':
+                            vflag = true;
+                            break;
+                        case 'f':
+                            fname = args[count++];
+                            break;
+                        case 'm':
+                            mname = args[count++];
+                            break;
+                        case '0':
+                            flag0 = true;
+                            break;
+                        case 'i':
+                            if (cflag || uflag || xflag || tflag) {
+                                usageError();
+                                return false;
+                            }
+                            // do not increase the counter, files will contain rootjar
+                            rootjar = args[count++];
+                            iflag = true;
+                            break;
+                        case 'n':
+                            nflag = true;
+                            break;
+                        case 'e':
+                            ename = args[count++];
+                            break;
+                        case 'P':
+                            pflag = true;
+                            break;
+                        default:
+                            error(formatMsg("error.illegal.option",
+                                    String.valueOf(flags.charAt(i))));
+                            usageError();
+                            return false;
                     }
-                    // do not increase the counter, files will contain rootjar
-                    rootjar = args[count++];
-                    iflag = true;
-                    break;
-                case 'n':
-                    nflag = true;
-                    break;
-                case 'e':
-                     ename = args[count++];
-                     break;
-                case 'P':
-                     pflag = true;
-                     break;
-                default:
-                    error(formatMsg("error.illegal.option",
-                                String.valueOf(flags.charAt(i))));
-                    usageError();
-                    return false;
                 }
             }
         } catch (ArrayIndexOutOfBoundsException e) {
             usageError();
             return false;
         }
-        if (!cflag && !tflag && !xflag && !uflag && !iflag) {
+
+        if (info != null) {
+            info.print(out);
+            return true;
+        }
+
+        if (!cflag && !tflag && !xflag && !uflag && !iflag && !printModuleDescriptor) {
             error(getMsg("error.bad.option"));
             usageError();
             return false;
@@ -489,23 +604,56 @@
         return true;
     }
 
+    private static Set<String> findPackages(ZipFile zf) {
+        return zf.stream()
+                 .filter(e -> e.getName().endsWith(".class"))
+                 .map(e -> toPackageName(e))
+                 .filter(pkg -> pkg.length() > 0)
+                 .distinct()
+                 .collect(Collectors.toSet());
+    }
+
+    private static String toPackageName(ZipEntry entry) {
+        return toPackageName(entry.getName());
+    }
+
+    private static String toPackageName(String path) {
+        assert path.endsWith(".class");
+        int index = path.lastIndexOf('/');
+        if (index != -1) {
+            return path.substring(0, index).replace('/', '.');
+        } else {
+            return "";
+        }
+    }
+
     /**
      * 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) {
-        if (files == null) {
+    void expand(File dir, String[] files, boolean isUpdate) throws IOException {
+        if (files == null)
             return;
-        }
+
         for (int i = 0; i < files.length; i++) {
             File f;
-            if (dir == null) {
+            if (dir == null)
                 f = new File(files[i]);
-            } else {
+            else
                 f = new File(dir, files[i]);
-            }
+
             if (f.isFile()) {
-                if (entries.add(f)) {
+                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);
                 }
@@ -529,13 +677,14 @@
     /**
      * Creates a new JAR file.
      */
-    void create(OutputStream out, Manifest manifest)
+    void create(OutputStream out, Manifest manifest, byte[] moduleInfoBytes)
         throws IOException
     {
         ZipOutputStream zos = new JarOutputStream(out);
         if (flag0) {
             zos.setMethod(ZipOutputStream.STORED);
         }
+        // TODO: check module-info attributes against manifest ??
         if (manifest != null) {
             if (vflag) {
                 output(getMsg("out.added.manifest"));
@@ -554,6 +703,20 @@
             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);
         }
@@ -589,12 +752,14 @@
      */
     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) {
@@ -606,6 +771,7 @@
             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)) {
@@ -633,6 +799,13 @@
                 if (!updateManifest(old, zos)) {
                     return false;
                 }
+            } else if (isModuleInfoEntry
+                       && ((newModuleInfoBytes != null) || (ename != null)
+                           || moduleVersion != null || dependenciesToHash != 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
@@ -653,6 +826,10 @@
                     entryMap.remove(name);
                     entries.remove(f);
                 }
+
+                jarEntries.add(name);
+                if (name.endsWith(".class"))
+                    packages.add(toPackageName(name));
             }
         }
 
@@ -675,6 +852,20 @@
                 }
             }
         }
+
+        // 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 || dependenciesToHash != null) {
+            error(getMsg("error.module.options.without.info"));
+            updateOk = false;
+        }
+
         zis.close();
         zos.close();
         return updateOk;
@@ -696,6 +887,22 @@
         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)
         throws IOException
     {
@@ -833,6 +1040,8 @@
                 output(formatMsg("out.ignore.entry", name));
             }
             return;
+        } else if (name.equals(MODULE_INFO)) {
+            throw new Error("Unexpected module info: " + name);
         }
 
         long size = isDir ? 0 : file.length();
@@ -928,6 +1137,17 @@
     }
 
     /**
+     * Computes the crc32 of a module-info.class.  This is necessary when the
+     * ZipOutputStream is in STORED mode.
+     */
+    private void crc32ModuleInfo(ZipEntry e, byte[] bytes) throws IOException {
+        CRC32OutputStream os = new CRC32OutputStream();
+        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
+        in.transferTo(os);
+        os.updateEntry(e);
+    }
+
+    /**
      * Computes the crc32 of a Manifest.  This is necessary when the
      * ZipOutputStream is in STORED mode.
      */
@@ -1151,7 +1371,7 @@
         try {
             if (update(Files.newInputStream(jarPath),
                        Files.newOutputStream(tmpPath),
-                       null, index)) {
+                       null, null, index)) {
                 try {
                     Files.move(tmpPath, jarPath, REPLACE_EXISTING);
                 } catch (IOException e) {
@@ -1268,7 +1488,7 @@
      * Prints usage message.
      */
     void usageError() {
-        error(getMsg("usage"));
+        Info.USAGE_SUMMARY.print(err);
     }
 
     /**
@@ -1369,4 +1589,235 @@
         }
         return tmpfile;
     }
+
+    private static byte[] readModuleInfo(InputStream zis) throws IOException {
+        return zis.readAllBytes();
+    }
+
+    private static byte[] readModuleInfo(Path path) throws IOException {
+        try (InputStream is = Files.newInputStream(path)) {
+            return is.readAllBytes();
+        }
+    }
+
+    // Modular jar support
+
+    static <T> String toString(Set<T> set,
+                               CharSequence prefix,
+                               CharSequence suffix ) {
+        if (set.isEmpty())
+            return "";
+
+        return set.stream().map(e -> e.toString())
+                           .collect(joining(", ", prefix, suffix));
+    }
+
+    private boolean printModuleDescriptor(ZipFile zipFile)
+        throws IOException
+    {
+        ZipEntry entry = zipFile.getEntry(MODULE_INFO);
+        if (entry ==  null)
+            return false;
+
+        try (InputStream is = zipFile.getInputStream(entry)) {
+            printModuleDescriptor(is);
+        }
+        return true;
+    }
+
+    private boolean printModuleDescriptor(FileInputStream fis)
+        throws IOException
+    {
+        try (BufferedInputStream bis = new BufferedInputStream(fis);
+             ZipInputStream zis = new ZipInputStream(bis)) {
+
+            ZipEntry e;
+            while ((e = zis.getNextEntry()) != null) {
+                if (e.getName().equals(MODULE_INFO)) {
+                    printModuleDescriptor(zis);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @SuppressWarnings("unchecked")
+    private void printModuleDescriptor(InputStream entryInputStream)
+        throws IOException
+    {
+        ModuleDescriptor md = ModuleDescriptor.read(entryInputStream);
+        StringBuilder sb = new StringBuilder();
+        sb.append("\nName:\n  " + md.toNameAndVersion());
+
+        Set<Requires> requires = md.requires();
+        if (!requires.isEmpty()) {
+            sb.append("\nRequires:");
+            requires.forEach(r ->
+                    sb.append("\n  ").append(r.name())
+                            .append(toString(r.modifiers(), " [ ", " ]")));
+        }
+
+        Set<String> s = md.uses();
+        if (!s.isEmpty()) {
+            sb.append("\nUses: ");
+            s.forEach(sv -> sb.append("\n  ").append(sv));
+        }
+
+        Set<Exports> exports = md.exports();
+        if (!exports.isEmpty()) {
+            sb.append("\nExports:");
+            exports.forEach(sv -> sb.append("\n  ").append(sv));
+        }
+
+        Map<String,Provides> provides = md.provides();
+        if (!provides.isEmpty()) {
+            sb.append("\nProvides: ");
+            provides.values().forEach(p ->
+                    sb.append("\n  ").append(p.service())
+                      .append(" with ")
+                      .append(toString(p.providers(), "", "")));
+        }
+
+        Optional<String> mc = md.mainClass();
+        if (mc.isPresent())
+            sb.append("\nMain class:\n  " + mc.get());
+
+        s = md.conceals();
+        if (!s.isEmpty()) {
+            sb.append("\nConceals:");
+            s.forEach(p -> sb.append("\n  ").append(p));
+        }
+
+        try {
+            Method m = ModuleDescriptor.class.getDeclaredMethod("hashes");
+            m.setAccessible(true);
+            Optional<Hasher.DependencyHashes> optHashes =
+                    (Optional<Hasher.DependencyHashes>) m.invoke(md);
+
+            if (optHashes.isPresent()) {
+                Hasher.DependencyHashes hashes = optHashes.get();
+                sb.append("\nHashes:");
+                sb.append("\n  Algorithm: " + hashes.algorithm());
+                hashes.names().stream().forEach(mod ->
+                        sb.append("\n  ").append(mod)
+                          .append(": ").append(hashes.hashFor(mod)));
+            }
+        } catch (ReflectiveOperationException x) {
+            throw new InternalError(x);
+        }
+        output(sb.toString());
+    }
+
+    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)
+                                .filter(p -> !jarEntries.contains(toBinaryName(p)))
+                                .collect(Collectors.toSet());
+        if (missing.size() > 0) {
+            missing.stream().forEach(s -> fatalError(formatMsg("error.missing.provider", s)));
+            return false;
+        }
+        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<ModuleDescriptor.Requires> dependences = md.requires();
+        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-dependencies
+            if (dependenciesToHash != null)
+                extender.hashes(hashDependences(name, dependences));
+
+            extender.write(baos);
+            return baos.toByteArray();
+        }
+    }
+
+    /**
+     * Examines the module dependences of the given module and computes the
+     * hash of any module that matches the pattern {@code dependenciesToHash}.
+     */
+    private Hasher.DependencyHashes
+    hashDependences(String name,
+                    Set<ModuleDescriptor.Requires> moduleDependences)
+        throws IOException
+    {
+        Map<String, Path> map = new HashMap<>();
+        Matcher matcher = dependenciesToHash.matcher("");
+        for (ModuleDescriptor.Requires md: moduleDependences) {
+            String dn = md.name();
+            if (matcher.reset(dn).find()) {
+                Optional<ModuleReference> omref = moduleFinder.find(dn);
+                if (!omref.isPresent()) {
+                    throw new IOException(formatMsg2("error.hash.dep", name , dn));
+                }
+                map.put(dn, modRefToPath(omref.get()));
+            }
+        }
+
+        if (map.size() == 0) {
+            return null;
+        } else {
+            return Hasher.generate(map, "SHA-256");
+        }
+    }
+
+    private static Path modRefToPath(ModuleReference mref) {
+        URI location = mref.location().get();
+        return Paths.get(location);
+    }
 }