1 /* 2 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.tools.jmod; 27 28 import java.io.ByteArrayInputStream; 29 import java.io.ByteArrayOutputStream; 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.OutputStream; 34 import java.io.PrintStream; 35 import java.io.PrintWriter; 36 import java.io.UncheckedIOException; 37 import java.lang.module.Configuration; 38 import java.lang.module.ModuleReader; 39 import java.lang.module.ModuleReference; 40 import java.lang.module.ModuleFinder; 41 import java.lang.module.ModuleDescriptor; 42 import java.lang.module.ModuleDescriptor.Exports; 43 import java.lang.module.ModuleDescriptor.Provides; 44 import java.lang.module.ModuleDescriptor.Requires; 45 import java.lang.module.ModuleDescriptor.Version; 46 import java.lang.module.ResolutionException; 47 import java.lang.module.ResolvedModule; 48 import java.net.URI; 49 import java.nio.file.FileSystems; 50 import java.nio.file.FileVisitResult; 51 import java.nio.file.Files; 52 import java.nio.file.InvalidPathException; 53 import java.nio.file.Path; 54 import java.nio.file.PathMatcher; 55 import java.nio.file.Paths; 56 import java.nio.file.SimpleFileVisitor; 57 import java.nio.file.StandardCopyOption; 58 import java.nio.file.attribute.BasicFileAttributes; 59 import java.text.MessageFormat; 60 import java.util.ArrayDeque; 61 import java.util.ArrayList; 62 import java.util.Collection; 63 import java.util.Collections; 64 import java.util.Comparator; 65 import java.util.Deque; 66 import java.util.HashMap; 67 import java.util.HashSet; 68 import java.util.List; 69 import java.util.Locale; 70 import java.util.Map; 71 import java.util.MissingResourceException; 72 import java.util.Optional; 73 import java.util.ResourceBundle; 74 import java.util.Set; 75 import java.util.function.Consumer; 76 import java.util.function.Function; 77 import java.util.function.Predicate; 78 import java.util.function.Supplier; 79 import java.util.jar.JarEntry; 80 import java.util.jar.JarFile; 81 import java.util.jar.JarOutputStream; 82 import java.util.stream.Collectors; 83 import java.util.regex.Pattern; 84 import java.util.regex.PatternSyntaxException; 85 import java.util.zip.ZipEntry; 86 import java.util.zip.ZipException; 87 import java.util.zip.ZipFile; 88 89 import jdk.internal.jmod.JmodFile; 90 import jdk.internal.jmod.JmodFile.Section; 91 import jdk.internal.joptsimple.BuiltinHelpFormatter; 92 import jdk.internal.joptsimple.NonOptionArgumentSpec; 93 import jdk.internal.joptsimple.OptionDescriptor; 94 import jdk.internal.joptsimple.OptionException; 95 import jdk.internal.joptsimple.OptionParser; 96 import jdk.internal.joptsimple.OptionSet; 97 import jdk.internal.joptsimple.OptionSpec; 98 import jdk.internal.joptsimple.ValueConverter; 99 import jdk.internal.misc.JavaLangModuleAccess; 100 import jdk.internal.misc.SharedSecrets; 101 import jdk.internal.module.ConfigurableModuleFinder; 102 import jdk.internal.module.ConfigurableModuleFinder.Phase; 103 import jdk.internal.module.ModuleHashes; 104 import jdk.internal.module.ModuleInfoExtender; 105 import jdk.tools.jlink.internal.Utils; 106 107 import static java.util.stream.Collectors.joining; 108 109 /** 110 * Implementation for the jmod tool. 111 */ 112 public class JmodTask { 113 114 static class CommandException extends RuntimeException { 115 private static final long serialVersionUID = 0L; 116 boolean showUsage; 117 118 CommandException(String key, Object... args) { 119 super(getMessageOrKey(key, args)); 120 } 121 122 CommandException showUsage(boolean b) { 123 showUsage = b; 124 return this; 125 } 126 127 private static String getMessageOrKey(String key, Object... args) { 128 try { 129 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 130 } catch (MissingResourceException e) { 131 return key; 132 } 133 } 134 } 135 136 private static final String PROGNAME = "jmod"; 137 private static final String MODULE_INFO = "module-info.class"; 138 139 private Options options; 140 private PrintWriter out = new PrintWriter(System.out, true); 141 void setLog(PrintWriter out, PrintWriter err) { 142 this.out = out; 143 } 144 145 /* Result codes. */ 146 static final int EXIT_OK = 0, // Completed with no errors. 147 EXIT_ERROR = 1, // Completed but reported errors. 148 EXIT_CMDERR = 2, // Bad command-line arguments 149 EXIT_SYSERR = 3, // System error or resource exhaustion. 150 EXIT_ABNORMAL = 4;// terminated abnormally 151 152 enum Mode { 153 CREATE, 154 LIST, 155 DESCRIBE, 156 HASH 157 }; 158 159 static class Options { 160 Mode mode; 161 Path jmodFile; 162 boolean help; 163 boolean version; 164 List<Path> classpath; 165 List<Path> cmds; 166 List<Path> configs; 167 List<Path> libs; 168 ModuleFinder moduleFinder; 169 Version moduleVersion; 170 String mainClass; 171 String osName; 172 String osArch; 173 String osVersion; 174 Pattern modulesToHash; 175 boolean dryrun; 176 List<PathMatcher> excludes; 177 } 178 179 public int run(String[] args) { 180 181 try { 182 handleOptions(args); 183 if (options == null) { 184 showUsageSummary(); 185 return EXIT_CMDERR; 186 } 187 if (options.help) { 188 showHelp(); 189 return EXIT_OK; 190 } 191 if (options.version) { 192 showVersion(); 193 return EXIT_OK; 194 } 195 196 boolean ok; 197 switch (options.mode) { 198 case CREATE: 199 ok = create(); 200 break; 201 case LIST: 202 ok = list(); 203 break; 204 case DESCRIBE: 205 ok = describe(); 206 break; 207 case HASH: 208 ok = hashModules(); 209 break; 210 default: 211 throw new AssertionError("Unknown mode: " + options.mode.name()); 212 } 213 214 return ok ? EXIT_OK : EXIT_ERROR; 215 } catch (CommandException e) { 216 reportError(e.getMessage()); 217 if (e.showUsage) 218 showUsageSummary(); 219 return EXIT_CMDERR; 220 } catch (Exception x) { 221 reportError(x.getMessage()); 222 x.printStackTrace(); 223 return EXIT_ABNORMAL; 224 } finally { 225 out.flush(); 226 } 227 } 228 229 private boolean list() throws IOException { 230 ZipFile zip = null; 231 try { 232 try { 233 zip = new ZipFile(options.jmodFile.toFile()); 234 } catch (IOException x) { 235 throw new IOException("error opening jmod file", x); 236 } 237 238 // Trivially print the archive entries for now, pending a more complete implementation 239 zip.stream().forEach(e -> out.println(e.getName())); 240 return true; 241 } finally { 242 if (zip != null) 243 zip.close(); 244 } 245 } 246 247 private boolean hashModules() { 248 return new Hasher(options.moduleFinder).run(); 249 } 250 251 private boolean describe() throws IOException { 252 try (JmodFile jf = new JmodFile(options.jmodFile)) { 253 try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) { 254 ModuleDescriptor md = ModuleDescriptor.read(in); 255 printModuleDescriptor(md); 256 return true; 257 } catch (IOException e) { 258 throw new CommandException("err.module.descriptor.not.found"); 259 } 260 } 261 } 262 263 static <T> String toString(Set<T> set) { 264 if (set.isEmpty()) { return ""; } 265 return set.stream().map(e -> e.toString().toLowerCase(Locale.ROOT)) 266 .collect(joining(" ")); 267 } 268 269 private static final JavaLangModuleAccess JLMA = SharedSecrets.getJavaLangModuleAccess(); 270 271 private void printModuleDescriptor(ModuleDescriptor md) 272 throws IOException 273 { 274 StringBuilder sb = new StringBuilder(); 275 sb.append("\n").append(md.toNameAndVersion()); 276 277 md.requires().stream() 278 .sorted(Comparator.comparing(Requires::name)) 279 .forEach(r -> { 280 sb.append("\n requires "); 281 if (!r.modifiers().isEmpty()) 282 sb.append(toString(r.modifiers())).append(" "); 283 sb.append(r.name()); 284 }); 285 286 md.uses().stream().sorted() 287 .forEach(s -> sb.append("\n uses ").append(s)); 288 289 md.exports().stream() 290 .sorted(Comparator.comparing(Exports::source)) 291 .forEach(p -> sb.append("\n exports ").append(p)); 292 293 md.conceals().stream().sorted() 294 .forEach(p -> sb.append("\n conceals ").append(p)); 295 296 md.provides().values().stream() 297 .sorted(Comparator.comparing(Provides::service)) 298 .forEach(p -> sb.append("\n provides ").append(p.service()) 299 .append(" with ") 300 .append(toString(p.providers()))); 301 302 md.mainClass().ifPresent(v -> sb.append("\n main-class " + v)); 303 304 md.osName().ifPresent(v -> sb.append("\n operating-system-name " + v)); 305 306 md.osArch().ifPresent(v -> sb.append("\n operating-system-architecture " + v)); 307 308 md.osVersion().ifPresent(v -> sb.append("\n operating-system-version " + v)); 309 310 JLMA.hashes(md).ifPresent( 311 hashes -> hashes.names().stream().sorted().forEach( 312 mod -> sb.append("\n hashes ").append(mod).append(" ") 313 .append(hashes.algorithm()).append(" ") 314 .append(hashes.hashFor(mod)))); 315 316 out.println(sb.toString()); 317 } 318 319 private boolean create() throws IOException { 320 JmodFileWriter jmod = new JmodFileWriter(); 321 322 // create jmod with temporary name to avoid it being examined 323 // when scanning the module path 324 Path target = options.jmodFile; 325 Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp"); 326 try { 327 try (JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) { 328 jmod.write(jos); 329 } 330 Files.move(tempTarget, target); 331 } catch (Exception e) { 332 if (Files.exists(tempTarget)) { 333 try { 334 Files.delete(tempTarget); 335 } catch (IOException ioe) { 336 e.addSuppressed(ioe); 337 } 338 } 339 throw e; 340 } 341 return true; 342 } 343 344 private class JmodFileWriter { 345 final List<Path> cmds = options.cmds; 346 final List<Path> libs = options.libs; 347 final List<Path> configs = options.configs; 348 final List<Path> classpath = options.classpath; 349 final Version moduleVersion = options.moduleVersion; 350 final String mainClass = options.mainClass; 351 final String osName = options.osName; 352 final String osArch = options.osArch; 353 final String osVersion = options.osVersion; 354 final List<PathMatcher> excludes = options.excludes; 355 final Hasher hasher = hasher(); 356 357 JmodFileWriter() { } 358 359 /** 360 * Writes the jmod to the given output stream. 361 */ 362 void write(JmodOutputStream out) throws IOException { 363 // module-info.class 364 writeModuleInfo(out, findPackages(classpath)); 365 366 // classes 367 processClasses(out, classpath); 368 369 processSection(out, Section.NATIVE_CMDS, cmds); 370 processSection(out, Section.NATIVE_LIBS, libs); 371 processSection(out, Section.CONFIG, configs); 372 } 373 374 /** 375 * Returns a supplier of an input stream to the module-info.class 376 * on the class path of directories and JAR files. 377 */ 378 Supplier<InputStream> newModuleInfoSupplier() throws IOException { 379 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 380 for (Path e: classpath) { 381 if (Files.isDirectory(e)) { 382 Path mi = e.resolve(MODULE_INFO); 383 if (Files.isRegularFile(mi)) { 384 Files.copy(mi, baos); 385 break; 386 } 387 } else if (Files.isRegularFile(e) && e.toString().endsWith(".jar")) { 388 try (JarFile jf = new JarFile(e.toFile())) { 389 ZipEntry entry = jf.getEntry(MODULE_INFO); 390 if (entry != null) { 391 jf.getInputStream(entry).transferTo(baos); 392 break; 393 } 394 } catch (ZipException x) { 395 // Skip. Do nothing. No packages will be added. 396 } 397 } 398 } 399 if (baos.size() == 0) { 400 return null; 401 } else { 402 byte[] bytes = baos.toByteArray(); 403 return () -> new ByteArrayInputStream(bytes); 404 } 405 } 406 407 /** 408 * Writes the updated module-info.class to the ZIP output stream. 409 * 410 * The updated module-info.class will have a ConcealedPackages attribute 411 * with the set of module-private/non-exported packages. 412 * 413 * If --module-version, --main-class, or other options were provided 414 * then the corresponding class file attributes are added to the 415 * module-info here. 416 */ 417 void writeModuleInfo(JmodOutputStream out, Set<String> packages) 418 throws IOException 419 { 420 Supplier<InputStream> miSupplier = newModuleInfoSupplier(); 421 if (miSupplier == null) { 422 throw new IOException(MODULE_INFO + " not found"); 423 } 424 425 ModuleDescriptor descriptor; 426 try (InputStream in = miSupplier.get()) { 427 descriptor = ModuleDescriptor.read(in); 428 } 429 430 // copy the module-info.class into the jmod with the additional 431 // attributes for the version, main class and other meta data 432 try (InputStream in = miSupplier.get()) { 433 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); 434 435 // Add (or replace) the ConcealedPackages attribute 436 if (packages != null) { 437 Set<String> exported = descriptor.exports().stream() 438 .map(ModuleDescriptor.Exports::source) 439 .collect(Collectors.toSet()); 440 Set<String> concealed = packages.stream() 441 .filter(p -> !exported.contains(p)) 442 .collect(Collectors.toSet()); 443 extender.conceals(concealed); 444 } 445 446 // --main-class 447 if (mainClass != null) 448 extender.mainClass(mainClass); 449 450 // --os-name, --os-arch, --os-version 451 if (osName != null || osArch != null || osVersion != null) 452 extender.targetPlatform(osName, osArch, osVersion); 453 454 // --module-version 455 if (moduleVersion != null) 456 extender.version(moduleVersion); 457 458 if (hasher != null) { 459 ModuleHashes moduleHashes = hasher.computeHashes(descriptor.name()); 460 if (moduleHashes != null) { 461 extender.hashes(moduleHashes); 462 } else { 463 warning("warn.no.module.hashes", descriptor.name()); 464 } 465 } 466 467 // write the (possibly extended or modified) module-info.class 468 out.writeEntry(extender.toByteArray(), Section.CLASSES, MODULE_INFO); 469 } 470 } 471 472 /* 473 * Hasher resolves a module graph using the --hash-modules PATTERN 474 * as the roots. 475 * 476 * The jmod file is being created and does not exist in the 477 * given modulepath. 478 */ 479 private Hasher hasher() { 480 if (options.modulesToHash == null) 481 return null; 482 483 try { 484 Supplier<InputStream> miSupplier = newModuleInfoSupplier(); 485 if (miSupplier == null) { 486 throw new IOException(MODULE_INFO + " not found"); 487 } 488 489 ModuleDescriptor descriptor; 490 try (InputStream in = miSupplier.get()) { 491 descriptor = ModuleDescriptor.read(in); 492 } 493 494 URI uri = options.jmodFile.toUri(); 495 ModuleReference mref = new ModuleReference(descriptor, uri, new Supplier<>() { 496 @Override 497 public ModuleReader get() { 498 throw new UnsupportedOperationException(); 499 } 500 }); 501 502 // compose a module finder with the module path and also 503 // a module finder that can find the jmod file being created 504 ModuleFinder finder = ModuleFinder.compose(options.moduleFinder, 505 new ModuleFinder() { 506 @Override 507 public Optional<ModuleReference> find(String name) { 508 if (descriptor.name().equals(name)) 509 return Optional.of(mref); 510 else return Optional.empty(); 511 } 512 513 @Override 514 public Set<ModuleReference> findAll() { 515 return Collections.singleton(mref); 516 } 517 }); 518 519 return new Hasher(finder); 520 } catch (IOException e) { 521 throw new UncheckedIOException(e); 522 } 523 } 524 525 /** 526 * Returns the set of all packages on the given class path. 527 */ 528 Set<String> findPackages(List<Path> classpath) { 529 Set<String> packages = new HashSet<>(); 530 for (Path path : classpath) { 531 if (Files.isDirectory(path)) { 532 packages.addAll(findPackages(path)); 533 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) { 534 try (JarFile jf = new JarFile(path.toString())) { 535 packages.addAll(findPackages(jf)); 536 } catch (ZipException x) { 537 // Skip. Do nothing. No packages will be added. 538 } catch (IOException ioe) { 539 throw new UncheckedIOException(ioe); 540 } 541 } 542 } 543 return packages; 544 } 545 546 /** 547 * Returns the set of packages in the given directory tree. 548 */ 549 Set<String> findPackages(Path dir) { 550 try { 551 return Files.find(dir, Integer.MAX_VALUE, 552 ((path, attrs) -> attrs.isRegularFile() && 553 path.toString().endsWith(".class"))) 554 .map(path -> toPackageName(dir.relativize(path))) 555 .filter(pkg -> pkg.length() > 0) // module-info 556 .distinct() 557 .collect(Collectors.toSet()); 558 } catch (IOException ioe) { 559 throw new UncheckedIOException(ioe); 560 } 561 } 562 563 /** 564 * Returns the set of packages in the given JAR file. 565 */ 566 Set<String> findPackages(JarFile jf) { 567 return jf.stream() 568 .filter(e -> e.getName().endsWith(".class")) 569 .map(e -> toPackageName(e)) 570 .filter(pkg -> pkg.length() > 0) // module-info 571 .distinct() 572 .collect(Collectors.toSet()); 573 } 574 575 String toPackageName(Path path) { 576 String name = path.toString(); 577 assert name.endsWith(".class"); 578 int index = name.lastIndexOf(File.separatorChar); 579 if (index != -1) 580 return name.substring(0, index).replace(File.separatorChar, '.'); 581 582 if (!name.equals(MODULE_INFO)) { 583 IOException e = new IOException(name + " in the unnamed package"); 584 throw new UncheckedIOException(e); 585 } 586 return ""; 587 } 588 589 String toPackageName(ZipEntry entry) { 590 String name = entry.getName(); 591 assert name.endsWith(".class"); 592 int index = name.lastIndexOf("/"); 593 if (index != -1) 594 return name.substring(0, index).replace('/', '.'); 595 else 596 return ""; 597 } 598 599 void processClasses(JmodOutputStream zos, List<Path> classpaths) 600 throws IOException 601 { 602 if (classpaths == null) 603 return; 604 605 for (Path p : classpaths) { 606 if (Files.isDirectory(p)) { 607 processSection(zos, Section.CLASSES, p); 608 } else if (Files.isRegularFile(p) && p.toString().endsWith(".jar")) { 609 try (JarFile jf = new JarFile(p.toFile())) { 610 JarEntryConsumer jec = new JarEntryConsumer(zos, jf); 611 jf.stream().filter(jec).forEach(jec); 612 } 613 } 614 } 615 } 616 617 void processSection(JmodOutputStream zos, Section section, List<Path> paths) 618 throws IOException 619 { 620 if (paths == null) 621 return; 622 623 for (Path p : paths) 624 processSection(zos, section, p); 625 } 626 627 void processSection(JmodOutputStream out, Section section, Path top) 628 throws IOException 629 { 630 Files.walkFileTree(top, new SimpleFileVisitor<Path>() { 631 @Override 632 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 633 throws IOException 634 { 635 Path relPath = top.relativize(file); 636 if (relPath.toString().equals(MODULE_INFO) 637 && !Section.CLASSES.equals(section)) 638 warning("warn.ignore.entry", MODULE_INFO, section); 639 640 if (!relPath.toString().equals(MODULE_INFO) 641 && !matches(relPath, excludes)) { 642 try (InputStream in = Files.newInputStream(file)) { 643 out.writeEntry(in, section, relPath.toString()); 644 } catch (IOException x) { 645 if (x.getMessage().contains("duplicate entry")) { 646 warning("warn.ignore.duplicate.entry", relPath.toString(), section); 647 return FileVisitResult.CONTINUE; 648 } 649 throw x; 650 } 651 } 652 return FileVisitResult.CONTINUE; 653 } 654 }); 655 } 656 657 boolean matches(Path path, List<PathMatcher> matchers) { 658 if (matchers != null) { 659 for (PathMatcher pm : matchers) { 660 if (pm.matches(path)) 661 return true; 662 } 663 } 664 return false; 665 } 666 667 class JarEntryConsumer implements Consumer<JarEntry>, Predicate<JarEntry> { 668 final JmodOutputStream out; 669 final JarFile jarfile; 670 JarEntryConsumer(JmodOutputStream out, JarFile jarfile) { 671 this.out = out; 672 this.jarfile = jarfile; 673 } 674 @Override 675 public void accept(JarEntry je) { 676 try (InputStream in = jarfile.getInputStream(je)) { 677 out.writeEntry(in, Section.CLASSES, je.getName()); 678 } catch (IOException e) { 679 throw new UncheckedIOException(e); 680 } 681 } 682 @Override 683 public boolean test(JarEntry je) { 684 String name = je.getName(); 685 // ## no support for excludes. Is it really needed? 686 return !name.endsWith(MODULE_INFO) && !je.isDirectory(); 687 } 688 } 689 } 690 691 /** 692 * Compute and record hashes 693 */ 694 private class Hasher { 695 final ModuleFinder moduleFinder; 696 final Map<String, Path> moduleNameToPath; 697 final Set<String> modules; 698 final Configuration configuration; 699 final boolean dryrun = options.dryrun; 700 Hasher(ModuleFinder finder) { 701 this.moduleFinder = finder; 702 // Determine the modules that matches the pattern {@code modulesToHash} 703 this.modules = moduleFinder.findAll().stream() 704 .map(mref -> mref.descriptor().name()) 705 .filter(mn -> options.modulesToHash.matcher(mn).find()) 706 .collect(Collectors.toSet()); 707 708 // a map from a module name to Path of the packaged module 709 this.moduleNameToPath = moduleFinder.findAll().stream() 710 .map(mref -> mref.descriptor().name()) 711 .collect(Collectors.toMap(Function.identity(), mn -> moduleToPath(mn))); 712 713 // get a resolved module graph 714 Configuration config = null; 715 try { 716 config = Configuration.empty() 717 .resolveRequires(ModuleFinder.ofSystem(), moduleFinder, modules); 718 } catch (ResolutionException e) { 719 warning("warn.module.resolution.fail", e.getMessage()); 720 } 721 this.configuration = config; 722 } 723 724 /** 725 * This method is for jmod hash command. 726 * 727 * Identify the base modules in the module graph, i.e. no outgoing edge 728 * to any of the modules to be hashed. 729 * 730 * For each base module M, compute the hashes of all modules that depend 731 * upon M directly or indirectly. Then update M's module-info.class 732 * to record the hashes. 733 */ 734 boolean run() { 735 if (configuration == null) 736 return false; 737 738 // transposed graph containing the the packaged modules and 739 // its transitive dependences matching --hash-modules 740 Map<String, Set<String>> graph = new HashMap<>(); 741 for (String root : modules) { 742 Deque<String> deque = new ArrayDeque<>(); 743 deque.add(root); 744 Set<String> visited = new HashSet<>(); 745 while (!deque.isEmpty()) { 746 String mn = deque.pop(); 747 if (!visited.contains(mn)) { 748 visited.add(mn); 749 750 if (modules.contains(mn)) 751 graph.computeIfAbsent(mn, _k -> new HashSet<>()); 752 753 ResolvedModule resolvedModule = configuration.findModule(mn).get(); 754 for (ResolvedModule dm : resolvedModule.reads()) { 755 String name = dm.name(); 756 if (!visited.contains(name)) { 757 deque.push(name); 758 } 759 760 // reverse edge 761 if (modules.contains(name) && modules.contains(mn)) { 762 graph.computeIfAbsent(name, _k -> new HashSet<>()).add(mn); 763 } 764 } 765 } 766 } 767 } 768 769 if (dryrun) 770 out.println("Dry run:"); 771 772 // each node in a transposed graph is a matching packaged module 773 // in which the hash of the modules that depend upon it is recorded 774 graph.entrySet().stream() 775 .filter(e -> !e.getValue().isEmpty()) 776 .forEach(e -> { 777 String mn = e.getKey(); 778 Map<String, Path> modulesForHash = e.getValue().stream() 779 .collect(Collectors.toMap(Function.identity(), 780 moduleNameToPath::get)); 781 ModuleHashes hashes = ModuleHashes.generate(modulesForHash, "SHA-256"); 782 if (dryrun) { 783 out.format("%s%n", mn); 784 hashes.names().stream() 785 .sorted() 786 .forEach(name -> out.format(" hashes %s %s %s%n", 787 name, hashes.algorithm(), hashes.hashFor(name))); 788 } else { 789 try { 790 updateModuleInfo(mn, hashes); 791 } catch (IOException ex) { 792 throw new UncheckedIOException(ex); 793 } 794 } 795 }); 796 return true; 797 } 798 799 /** 800 * Compute hashes of the specified module. 801 * 802 * It records the hashing modules that depend upon the specified 803 * module directly or indirectly. 804 */ 805 ModuleHashes computeHashes(String name) { 806 if (configuration == null) 807 return null; 808 809 // the transposed graph includes all modules in the resolved graph 810 Map<String, Set<String>> graph = transpose(); 811 812 // find the modules that transitively depend upon the specified name 813 Deque<String> deque = new ArrayDeque<>(); 814 deque.add(name); 815 Set<String> mods = visitNodes(graph, deque); 816 817 // filter modules matching the pattern specified --hash-modules 818 // as well as itself as the jmod file is being generated 819 Map<String, Path> modulesForHash = mods.stream() 820 .filter(mn -> !mn.equals(name) && modules.contains(mn)) 821 .collect(Collectors.toMap(Function.identity(), moduleNameToPath::get)); 822 823 if (modulesForHash.isEmpty()) 824 return null; 825 826 return ModuleHashes.generate(modulesForHash, "SHA-256"); 827 } 828 829 /** 830 * Returns all nodes traversed from the given roots. 831 */ 832 private Set<String> visitNodes(Map<String, Set<String>> graph, 833 Deque<String> roots) { 834 Set<String> visited = new HashSet<>(); 835 while (!roots.isEmpty()) { 836 String mn = roots.pop(); 837 if (!visited.contains(mn)) { 838 visited.add(mn); 839 // the given roots may not be part of the graph 840 if (graph.containsKey(mn)) { 841 for (String dm : graph.get(mn)) { 842 if (!visited.contains(dm)) { 843 roots.push(dm); 844 } 845 } 846 } 847 } 848 } 849 return visited; 850 } 851 852 /** 853 * Returns a transposed graph from the resolved module graph. 854 */ 855 private Map<String, Set<String>> transpose() { 856 Map<String, Set<String>> transposedGraph = new HashMap<>(); 857 Deque<String> deque = new ArrayDeque<>(modules); 858 859 Set<String> visited = new HashSet<>(); 860 while (!deque.isEmpty()) { 861 String mn = deque.pop(); 862 if (!visited.contains(mn)) { 863 visited.add(mn); 864 865 transposedGraph.computeIfAbsent(mn, _k -> new HashSet<>()); 866 867 ResolvedModule resolvedModule = configuration.findModule(mn).get(); 868 for (ResolvedModule dm : resolvedModule.reads()) { 869 String name = dm.name(); 870 if (!visited.contains(name)) { 871 deque.push(name); 872 } 873 874 // reverse edge 875 transposedGraph.computeIfAbsent(name, _k -> new HashSet<>()) 876 .add(mn); 877 } 878 } 879 } 880 return transposedGraph; 881 } 882 883 /** 884 * Reads the given input stream of module-info.class and write 885 * the extended module-info.class with the given ModuleHashes 886 * 887 * @param in InputStream of module-info.class 888 * @param out OutputStream to write the extended module-info.class 889 * @param hashes ModuleHashes 890 */ 891 private void recordHashes(InputStream in, OutputStream out, ModuleHashes hashes) 892 throws IOException 893 { 894 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); 895 extender.hashes(hashes); 896 extender.write(out); 897 } 898 899 private void updateModuleInfo(String name, ModuleHashes moduleHashes) 900 throws IOException 901 { 902 Path target = moduleNameToPath.get(name); 903 Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp"); 904 try { 905 if (target.getFileName().toString().endsWith(".jmod")) { 906 updateJmodFile(target, tempTarget, moduleHashes); 907 } else { 908 updateModularJar(target, tempTarget, moduleHashes); 909 } 910 } catch (IOException|RuntimeException e) { 911 if (Files.exists(tempTarget)) { 912 try { 913 Files.delete(tempTarget); 914 } catch (IOException ioe) { 915 e.addSuppressed(ioe); 916 } 917 } 918 throw e; 919 } 920 921 out.println(getMessage("module.hashes.recorded", name)); 922 Files.move(tempTarget, target, StandardCopyOption.REPLACE_EXISTING); 923 } 924 925 private void updateModularJar(Path target, Path tempTarget, 926 ModuleHashes moduleHashes) 927 throws IOException 928 { 929 try (JarFile jf = new JarFile(target.toFile()); 930 OutputStream out = Files.newOutputStream(tempTarget); 931 JarOutputStream jos = new JarOutputStream(out)) 932 { 933 jf.stream().forEach(e -> { 934 try (InputStream in = jf.getInputStream(e)) { 935 if (e.getName().equals(MODULE_INFO)) { 936 // what about module-info.class in versioned entries? 937 ZipEntry ze = new ZipEntry(e.getName()); 938 ze.setTime(System.currentTimeMillis()); 939 jos.putNextEntry(ze); 940 recordHashes(in, jos, moduleHashes); 941 jos.closeEntry(); 942 } else { 943 jos.putNextEntry(e); 944 jos.write(in.readAllBytes()); 945 jos.closeEntry(); 946 } 947 } catch (IOException x) { 948 throw new UncheckedIOException(x); 949 } 950 }); 951 } 952 } 953 954 private void updateJmodFile(Path target, Path tempTarget, 955 ModuleHashes moduleHashes) 956 throws IOException 957 { 958 959 try (JmodFile jf = new JmodFile(target); 960 JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) 961 { 962 jf.stream().forEach(e -> { 963 try (InputStream in = jf.getInputStream(e.section(), e.name())) { 964 if (e.name().equals(MODULE_INFO)) { 965 // replace module-info.class 966 ModuleInfoExtender extender = 967 ModuleInfoExtender.newExtender(in); 968 extender.hashes(moduleHashes); 969 jos.writeEntry(extender.toByteArray(), e.section(), e.name()); 970 } else { 971 jos.writeEntry(in, e); 972 } 973 } catch (IOException x) { 974 throw new UncheckedIOException(x); 975 } 976 }); 977 } 978 } 979 980 private Path moduleToPath(String name) { 981 ModuleReference mref = moduleFinder.find(name).orElseThrow( 982 () -> new InternalError("Selected module " + name + " not on module path")); 983 984 URI uri = mref.location().get(); 985 Path path = Paths.get(uri); 986 String fn = path.getFileName().toString(); 987 if (!fn.endsWith(".jar") && !fn.endsWith(".jmod")) { 988 throw new InternalError(path + " is not a modular JAR or jmod file"); 989 } 990 return path; 991 } 992 } 993 994 static class ClassPathConverter implements ValueConverter<Path> { 995 static final ValueConverter<Path> INSTANCE = new ClassPathConverter(); 996 997 private static final Path CWD = Paths.get(""); 998 999 @Override 1000 public Path convert(String value) { 1001 try { 1002 Path path = CWD.resolve(value); 1003 if (Files.notExists(path)) 1004 throw new CommandException("err.path.not.found", path); 1005 if (! (Files.isDirectory(path) || 1006 (Files.isRegularFile(path) && path.toString().endsWith(".jar")))) 1007 throw new CommandException("err.invalid.class.path.entry", path); 1008 return path; 1009 } catch (InvalidPathException x) { 1010 throw new CommandException("err.path.not.valid", value); 1011 } 1012 } 1013 1014 @Override public Class<Path> valueType() { return Path.class; } 1015 1016 @Override public String valuePattern() { return "path"; } 1017 } 1018 1019 static class DirPathConverter implements ValueConverter<Path> { 1020 static final ValueConverter<Path> INSTANCE = new DirPathConverter(); 1021 1022 private static final Path CWD = Paths.get(""); 1023 1024 @Override 1025 public Path convert(String value) { 1026 try { 1027 Path path = CWD.resolve(value); 1028 if (Files.notExists(path)) 1029 throw new CommandException("err.path.not.found", path); 1030 if (!Files.isDirectory(path)) 1031 throw new CommandException("err.path.not.a.dir", path); 1032 return path; 1033 } catch (InvalidPathException x) { 1034 throw new CommandException("err.path.not.valid", value); 1035 } 1036 } 1037 1038 @Override public Class<Path> valueType() { return Path.class; } 1039 1040 @Override public String valuePattern() { return "path"; } 1041 } 1042 1043 static class ModuleVersionConverter implements ValueConverter<Version> { 1044 @Override 1045 public Version convert(String value) { 1046 try { 1047 return Version.parse(value); 1048 } catch (IllegalArgumentException x) { 1049 throw new CommandException("err.invalid.version", x.getMessage()); 1050 } 1051 } 1052 1053 @Override public Class<Version> valueType() { return Version.class; } 1054 1055 @Override public String valuePattern() { return "module-version"; } 1056 } 1057 1058 static class PatternConverter implements ValueConverter<Pattern> { 1059 @Override 1060 public Pattern convert(String value) { 1061 try { 1062 if (value.startsWith("regex:")) { 1063 value = value.substring("regex:".length()).trim(); 1064 } 1065 1066 return Pattern.compile(value); 1067 } catch (PatternSyntaxException e) { 1068 throw new CommandException("err.bad.pattern", value); 1069 } 1070 } 1071 1072 @Override public Class<Pattern> valueType() { return Pattern.class; } 1073 1074 @Override public String valuePattern() { return "regex-pattern"; } 1075 } 1076 1077 static class PathMatcherConverter implements ValueConverter<PathMatcher> { 1078 @Override 1079 public PathMatcher convert(String pattern) { 1080 try { 1081 return Utils.getPathMatcher(FileSystems.getDefault(), pattern); 1082 } catch (PatternSyntaxException e) { 1083 throw new CommandException("err.bad.pattern", pattern); 1084 } 1085 } 1086 1087 @Override public Class<PathMatcher> valueType() { return PathMatcher.class; } 1088 1089 @Override public String valuePattern() { return "pattern-list"; } 1090 } 1091 1092 /* Support for @<file> in jmod help */ 1093 private static final String CMD_FILENAME = "@<filename>"; 1094 1095 /** 1096 * This formatter is adding the @filename option and does the required 1097 * formatting. 1098 */ 1099 private static final class JmodHelpFormatter extends BuiltinHelpFormatter { 1100 1101 private JmodHelpFormatter() { super(80, 2); } 1102 1103 @Override 1104 public String format(Map<String, ? extends OptionDescriptor> options) { 1105 Map<String, OptionDescriptor> all = new HashMap<>(); 1106 all.putAll(options); 1107 all.put(CMD_FILENAME, new OptionDescriptor() { 1108 @Override 1109 public Collection<String> options() { 1110 List<String> ret = new ArrayList<>(); 1111 ret.add(CMD_FILENAME); 1112 return ret; 1113 } 1114 @Override 1115 public String description() { return getMessage("main.opt.cmdfile"); } 1116 @Override 1117 public List<?> defaultValues() { return Collections.emptyList(); } 1118 @Override 1119 public boolean isRequired() { return false; } 1120 @Override 1121 public boolean acceptsArguments() { return false; } 1122 @Override 1123 public boolean requiresArgument() { return false; } 1124 @Override 1125 public String argumentDescription() { return null; } 1126 @Override 1127 public String argumentTypeIndicator() { return null; } 1128 @Override 1129 public boolean representsNonOptions() { return false; } 1130 }); 1131 String content = super.format(all); 1132 StringBuilder builder = new StringBuilder(); 1133 1134 builder.append(getMessage("main.opt.mode")).append("\n "); 1135 builder.append(getMessage("main.opt.mode.create")).append("\n "); 1136 builder.append(getMessage("main.opt.mode.list")).append("\n "); 1137 builder.append(getMessage("main.opt.mode.describe")).append("\n "); 1138 builder.append(getMessage("main.opt.mode.hash")).append("\n\n"); 1139 1140 String cmdfile = null; 1141 String[] lines = content.split("\n"); 1142 for (String line : lines) { 1143 if (line.startsWith("--@")) { 1144 cmdfile = line.replace("--" + CMD_FILENAME, CMD_FILENAME + " "); 1145 } else if (line.startsWith("Option") || line.startsWith("------")) { 1146 builder.append(" ").append(line).append("\n"); 1147 } else if (!line.matches("Non-option arguments")){ 1148 builder.append(" ").append(line).append("\n"); 1149 } 1150 } 1151 if (cmdfile != null) { 1152 builder.append(" ").append(cmdfile).append("\n"); 1153 } 1154 return builder.toString(); 1155 } 1156 } 1157 1158 private final OptionParser parser = new OptionParser("hp"); 1159 1160 private void handleOptions(String[] args) { 1161 parser.formatHelpWith(new JmodHelpFormatter()); 1162 1163 OptionSpec<Path> classPath 1164 = parser.accepts("class-path", getMessage("main.opt.class-path")) 1165 .withRequiredArg() 1166 .withValuesSeparatedBy(File.pathSeparatorChar) 1167 .withValuesConvertedBy(ClassPathConverter.INSTANCE); 1168 1169 OptionSpec<Path> cmds 1170 = parser.accepts("cmds", getMessage("main.opt.cmds")) 1171 .withRequiredArg() 1172 .withValuesSeparatedBy(File.pathSeparatorChar) 1173 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1174 1175 OptionSpec<Path> config 1176 = parser.accepts("config", getMessage("main.opt.config")) 1177 .withRequiredArg() 1178 .withValuesSeparatedBy(File.pathSeparatorChar) 1179 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1180 1181 OptionSpec<Void> dryrun 1182 = parser.accepts("dry-run", getMessage("main.opt.dry-run")); 1183 1184 OptionSpec<PathMatcher> excludes 1185 = parser.accepts("exclude", getMessage("main.opt.exclude")) 1186 .withRequiredArg() 1187 .withValuesConvertedBy(new PathMatcherConverter()); 1188 1189 OptionSpec<Pattern> hashModules 1190 = parser.accepts("hash-modules", getMessage("main.opt.hash-modules")) 1191 .withRequiredArg() 1192 .withValuesConvertedBy(new PatternConverter()); 1193 1194 OptionSpec<Void> help 1195 = parser.acceptsAll(Set.of("h", "help"), getMessage("main.opt.help")) 1196 .forHelp(); 1197 1198 OptionSpec<Path> libs 1199 = parser.accepts("libs", getMessage("main.opt.libs")) 1200 .withRequiredArg() 1201 .withValuesSeparatedBy(File.pathSeparatorChar) 1202 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1203 1204 OptionSpec<String> mainClass 1205 = parser.accepts("main-class", getMessage("main.opt.main-class")) 1206 .withRequiredArg() 1207 .describedAs(getMessage("main.opt.main-class.arg")); 1208 1209 OptionSpec<Path> modulePath 1210 = parser.acceptsAll(Set.of("p", "module-path"), 1211 getMessage("main.opt.module-path")) 1212 .withRequiredArg() 1213 .withValuesSeparatedBy(File.pathSeparatorChar) 1214 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1215 1216 OptionSpec<Version> moduleVersion 1217 = parser.accepts("module-version", getMessage("main.opt.module-version")) 1218 .withRequiredArg() 1219 .withValuesConvertedBy(new ModuleVersionConverter()); 1220 1221 OptionSpec<String> osName 1222 = parser.accepts("os-name", getMessage("main.opt.os-name")) 1223 .withRequiredArg() 1224 .describedAs(getMessage("main.opt.os-name.arg")); 1225 1226 OptionSpec<String> osArch 1227 = parser.accepts("os-arch", getMessage("main.opt.os-arch")) 1228 .withRequiredArg() 1229 .describedAs(getMessage("main.opt.os-arch.arg")); 1230 1231 OptionSpec<String> osVersion 1232 = parser.accepts("os-version", getMessage("main.opt.os-version")) 1233 .withRequiredArg() 1234 .describedAs(getMessage("main.opt.os-version.arg")); 1235 1236 OptionSpec<Void> version 1237 = parser.accepts("version", getMessage("main.opt.version")); 1238 1239 NonOptionArgumentSpec<String> nonOptions 1240 = parser.nonOptions(); 1241 1242 try { 1243 OptionSet opts = parser.parse(args); 1244 1245 if (opts.has(help) || opts.has(version)) { 1246 options = new Options(); 1247 options.help = opts.has(help); 1248 options.version = opts.has(version); 1249 return; // informational message will be shown 1250 } 1251 1252 List<String> words = opts.valuesOf(nonOptions); 1253 if (words.isEmpty()) 1254 throw new CommandException("err.missing.mode").showUsage(true); 1255 String verb = words.get(0); 1256 options = new Options(); 1257 try { 1258 options.mode = Enum.valueOf(Mode.class, verb.toUpperCase()); 1259 } catch (IllegalArgumentException e) { 1260 throw new CommandException("err.invalid.mode", verb).showUsage(true); 1261 } 1262 1263 if (opts.has(classPath)) 1264 options.classpath = opts.valuesOf(classPath); 1265 if (opts.has(cmds)) 1266 options.cmds = opts.valuesOf(cmds); 1267 if (opts.has(config)) 1268 options.configs = opts.valuesOf(config); 1269 if (opts.has(dryrun)) 1270 options.dryrun = true; 1271 if (opts.has(excludes)) 1272 options.excludes = opts.valuesOf(excludes); 1273 if (opts.has(libs)) 1274 options.libs = opts.valuesOf(libs); 1275 if (opts.has(modulePath)) { 1276 Path[] dirs = opts.valuesOf(modulePath).toArray(new Path[0]); 1277 options.moduleFinder = ModuleFinder.of(dirs); 1278 if (options.moduleFinder instanceof ConfigurableModuleFinder) 1279 ((ConfigurableModuleFinder)options.moduleFinder).configurePhase(Phase.LINK_TIME); 1280 } 1281 if (opts.has(moduleVersion)) 1282 options.moduleVersion = opts.valueOf(moduleVersion); 1283 if (opts.has(mainClass)) 1284 options.mainClass = opts.valueOf(mainClass); 1285 if (opts.has(osName)) 1286 options.osName = opts.valueOf(osName); 1287 if (opts.has(osArch)) 1288 options.osArch = opts.valueOf(osArch); 1289 if (opts.has(osVersion)) 1290 options.osVersion = opts.valueOf(osVersion); 1291 if (opts.has(hashModules)) { 1292 options.modulesToHash = opts.valueOf(hashModules); 1293 // if storing hashes then the module path is required 1294 if (options.moduleFinder == null) 1295 throw new CommandException("err.modulepath.must.be.specified") 1296 .showUsage(true); 1297 } 1298 1299 if (options.mode.equals(Mode.HASH)) { 1300 if (options.moduleFinder == null || options.modulesToHash == null) 1301 throw new CommandException("err.modulepath.must.be.specified") 1302 .showUsage(true); 1303 } else { 1304 if (words.size() <= 1) 1305 throw new CommandException("err.jmod.must.be.specified").showUsage(true); 1306 Path path = Paths.get(words.get(1)); 1307 1308 if (options.mode.equals(Mode.CREATE) && Files.exists(path)) 1309 throw new CommandException("err.file.already.exists", path); 1310 else if ((options.mode.equals(Mode.LIST) || 1311 options.mode.equals(Mode.DESCRIBE)) 1312 && Files.notExists(path)) 1313 throw new CommandException("err.jmod.not.found", path); 1314 1315 if (options.dryrun) { 1316 throw new CommandException("err.invalid.dryrun.option"); 1317 } 1318 options.jmodFile = path; 1319 1320 if (words.size() > 2) 1321 throw new CommandException("err.unknown.option", 1322 words.subList(2, words.size())).showUsage(true); 1323 } 1324 1325 if (options.mode.equals(Mode.CREATE) && options.classpath == null) 1326 throw new CommandException("err.classpath.must.be.specified").showUsage(true); 1327 if (options.mainClass != null && !isValidJavaIdentifier(options.mainClass)) 1328 throw new CommandException("err.invalid.main-class", options.mainClass); 1329 } catch (OptionException e) { 1330 throw new CommandException(e.getMessage()); 1331 } 1332 } 1333 1334 /** 1335 * Returns true if, and only if, the given main class is a legal. 1336 */ 1337 static boolean isValidJavaIdentifier(String mainClass) { 1338 if (mainClass.length() == 0) 1339 return false; 1340 1341 if (!Character.isJavaIdentifierStart(mainClass.charAt(0))) 1342 return false; 1343 1344 int n = mainClass.length(); 1345 for (int i=1; i < n; i++) { 1346 char c = mainClass.charAt(i); 1347 if (!Character.isJavaIdentifierPart(c) && c != '.') 1348 return false; 1349 } 1350 if (mainClass.charAt(n-1) == '.') 1351 return false; 1352 1353 return true; 1354 } 1355 1356 private void reportError(String message) { 1357 out.println(getMessage("error.prefix") + " " + message); 1358 } 1359 1360 private void warning(String key, Object... args) { 1361 out.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 1362 } 1363 1364 private void showUsageSummary() { 1365 out.println(getMessage("main.usage.summary", PROGNAME)); 1366 } 1367 1368 private void showHelp() { 1369 out.println(getMessage("main.usage", PROGNAME)); 1370 try { 1371 parser.printHelpOn(out); 1372 } catch (IOException x) { 1373 throw new AssertionError(x); 1374 } 1375 } 1376 1377 private void showVersion() { 1378 out.println(version()); 1379 } 1380 1381 private String version() { 1382 return System.getProperty("java.version"); 1383 } 1384 1385 private static String getMessage(String key, Object... args) { 1386 try { 1387 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 1388 } catch (MissingResourceException e) { 1389 throw new InternalError("Missing message: " + key); 1390 } 1391 } 1392 1393 private static class ResourceBundleHelper { 1394 static final ResourceBundle bundle; 1395 1396 static { 1397 Locale locale = Locale.getDefault(); 1398 try { 1399 bundle = ResourceBundle.getBundle("jdk.tools.jmod.resources.jmod", locale); 1400 } catch (MissingResourceException e) { 1401 throw new InternalError("Cannot find jmod resource bundle for locale " + locale); 1402 } 1403 } 1404 } 1405 }