#!/usr/bin/env python3 """Report which JDK modules the shipped jars reference but JRE_MODULES omits. python3 tests/module-scan.py [ ...] python3 tests/module-scan.py /opt/jdk /opt/suramadu /opt/megamek Why this exists: the jlink runtime keeps ~18 of the JDK's modules, and a missing one does not fail the build. It fails at runtime, often far from the cause - leaving out jdk.net let Suramadu bind its port and then throw NoClassDefFoundError on every accept(), which looks like a network problem rather than a missing module. Run it after bumping MegaMek or Suramadu, or when adding a dependency. Read the output as a starting point, not a shopping list. A static reference is not proof of runtime need: bundled libraries routinely reference javax.tools or java.rmi on code paths nothing takes. Adding all of them here would cost 14MB for four modules that have never been needed. Add one when something fails without it, and record why in versions.env. Not part of ./test.sh - it needs the extracted third-party trees, and it reports rather than passes or fails. """ import glob import os import re import subprocess import sys import zipfile # Kept in step with versions.env by hand. A mismatch shows up as noise in the # output rather than a wrong answer. CURRENT = { "java.base", "java.datatransfer", "java.xml", "java.prefs", "java.desktop", "java.logging", "java.management", "java.security.sasl", "java.naming", "java.scripting", "java.transaction.xa", "java.sql", "jdk.unsupported", "jdk.crypto.ec", "jdk.zipfs", "java.instrument", "jdk.net", "jdk.accessibility", "jdk.management", } # A capturing group here would make findall return just the group, so every # match collapses to "jdk" and the scan silently finds nothing. REF = re.compile(rb"(?:java|javax|jdk|sun|com/sun)/[a-zA-Z0-9_/$]+") def package_map(jdk): """package -> module, for every observable module in this JDK.""" mods = [ m.split("@")[0] for m in subprocess.run( [f"{jdk}/bin/java", "--list-modules"], capture_output=True, text=True, check=True, ).stdout.split() ] pkgs = {} for m in mods: out = subprocess.run( [f"{jdk}/bin/java", "--describe-module", m], capture_output=True, text=True ).stdout for line in out.splitlines(): line = line.strip() if line.startswith(("exports ", "contains ")): pkgs.setdefault(line.split()[1], m) return pkgs def jars_under(path): if os.path.isfile(path) and path.endswith(".jar"): return [path] return sorted(glob.glob(os.path.join(path, "**", "*.jar"), recursive=True)) def modules_referenced(jars, pkg2mod): found = set() for j in jars: try: z = zipfile.ZipFile(j) except Exception: continue for name in z.namelist(): if not name.endswith(".class"): continue try: data = z.read(name) except Exception: continue for ref in REF.findall(data): parts = ref.decode().split("/") # Longest matching package wins: java.nio.file before java.nio. for cut in range(len(parts) - 1, 1, -1): cand = ".".join(parts[:cut]) if cand in pkg2mod: found.add(pkg2mod[cand]) break return found def main(): if len(sys.argv) < 3: sys.exit(__doc__) jdk, targets = sys.argv[1], sys.argv[2:] pkg2mod = package_map(jdk) everything = set() for t in targets: jars = jars_under(t) mods = modules_referenced(jars, pkg2mod) missing = sorted(mods - CURRENT) print(f"\n{t} ({len(jars)} jars)") print(" referenced but not in JRE_MODULES:") for m in missing: print(f" {m}") if not missing: print(" (none)") everything |= set(missing) print("\nunion:", ",".join(sorted(everything)) or "(nothing missing)") print("\nMeasure before adding any of these:") print(f" {jdk}/bin/jlink --add-modules --strip-debug \\") print(" --no-man-pages --no-header-files --compress=zip-6 --output /tmp/jre") if __name__ == "__main__": main()