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())