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