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
41 import java.lang.module.ModuleDescriptor.Exports;
42 import java.lang.module.ModuleDescriptor.Opens;
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.FileVisitOption;
51 import java.nio.file.FileVisitResult;
52 import java.nio.file.Files;
53 import java.nio.file.InvalidPathException;
54 import java.nio.file.Path;
55 import java.nio.file.PathMatcher;
56 import java.nio.file.Paths;
57 import java.nio.file.SimpleFileVisitor;
58 import java.nio.file.StandardCopyOption;
59 import java.nio.file.attribute.BasicFileAttributes;
60 import java.text.MessageFormat;
61 import java.util.ArrayDeque;
62 import java.util.ArrayList;
63 import java.util.Collection;
64 import java.util.Collections;
65 import java.util.Comparator;
66 import java.util.Deque;
67 import java.util.HashMap;
68 import java.util.HashSet;
69 import java.util.LinkedHashMap;
70 import java.util.List;
71 import java.util.Locale;
72 import java.util.Map;
73 import java.util.MissingResourceException;
74 import java.util.Optional;
75 import java.util.ResourceBundle;
76 import java.util.Set;
77 import java.util.TreeSet;
78 import java.util.function.Consumer;
79 import java.util.function.Function;
80 import java.util.function.Predicate;
81 import java.util.function.Supplier;
82 import java.util.jar.JarEntry;
83 import java.util.jar.JarFile;
84 import java.util.jar.JarOutputStream;
85 import java.util.stream.Collectors;
86 import java.util.regex.Pattern;
87 import java.util.regex.PatternSyntaxException;
88 import java.util.zip.ZipEntry;
89 import java.util.zip.ZipException;
90 import java.util.zip.ZipFile;
91
92 import jdk.internal.jmod.JmodFile;
93 import jdk.internal.jmod.JmodFile.Section;
94 import jdk.internal.joptsimple.BuiltinHelpFormatter;
95 import jdk.internal.joptsimple.NonOptionArgumentSpec;
96 import jdk.internal.joptsimple.OptionDescriptor;
97 import jdk.internal.joptsimple.OptionException;
98 import jdk.internal.joptsimple.OptionParser;
99 import jdk.internal.joptsimple.OptionSet;
100 import jdk.internal.joptsimple.OptionSpec;
101 import jdk.internal.joptsimple.ValueConverter;
102 import jdk.internal.loader.ResourceHelper;
103 import jdk.internal.module.ModuleHashes;
104 import jdk.internal.module.ModuleInfo;
105 import jdk.internal.module.ModuleInfoExtender;
106 import jdk.internal.module.ModulePath;
107 import jdk.internal.module.ModuleResolution;
108 import jdk.tools.jlink.internal.Utils;
109
110 import static java.util.stream.Collectors.joining;
111
112 /**
113 * Implementation for the jmod tool.
114 */
115 public class JmodTask {
116
117 static class CommandException extends RuntimeException {
118 private static final long serialVersionUID = 0L;
119 boolean showUsage;
120
121 CommandException(String key, Object... args) {
122 super(getMessageOrKey(key, args));
123 }
269 int index = name.lastIndexOf("/");
270 if (index != -1) {
271 Path p = dir.resolve(name.substring(0, index));
272 if (Files.notExists(p))
273 Files.createDirectories(p);
274 }
275
276 try (OutputStream os = Files.newOutputStream(dir.resolve(name))) {
277 jf.getInputStream(e).transferTo(os);
278 }
279 } catch (IOException x) {
280 throw new UncheckedIOException(x);
281 }
282 });
283
284 return true;
285 }
286 }
287
288 private boolean hashModules() {
289 return new Hasher(options.moduleFinder).run();
290 }
291
292 private boolean describe() throws IOException {
293 try (JmodFile jf = new JmodFile(options.jmodFile)) {
294 try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) {
295 ModuleInfo.Attributes attrs = ModuleInfo.read(in, null);
296 printModuleDescriptor(attrs.descriptor(), attrs.recordedHashes());
297 return true;
298 } catch (IOException e) {
299 throw new CommandException("err.module.descriptor.not.found");
300 }
301 }
302 }
303
304 static <T> String toString(Collection<T> c) {
305 if (c.isEmpty()) { return ""; }
306 return c.stream().map(e -> e.toString().toLowerCase(Locale.ROOT))
307 .collect(joining(" "));
308 }
309
360 .append(toHex(hashes.hashFor(mod))));
361 }
362
363 out.println(sb.toString());
364 }
365
366 private String toHex(byte[] ba) {
367 StringBuilder sb = new StringBuilder(ba.length);
368 for (byte b: ba) {
369 sb.append(String.format("%02x", b & 0xff));
370 }
371 return sb.toString();
372 }
373
374 private boolean create() throws IOException {
375 JmodFileWriter jmod = new JmodFileWriter();
376
377 // create jmod with temporary name to avoid it being examined
378 // when scanning the module path
379 Path target = options.jmodFile;
380 Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp");
381 try {
382 try (JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) {
383 jmod.write(jos);
384 }
385 Files.move(tempTarget, target);
386 } catch (Exception e) {
387 if (Files.exists(tempTarget)) {
388 try {
389 Files.delete(tempTarget);
390 } catch (IOException ioe) {
391 e.addSuppressed(ioe);
392 }
393 }
394 throw e;
395 }
396 return true;
397 }
398
399 private class JmodFileWriter {
400 final List<Path> cmds = options.cmds;
401 final List<Path> libs = options.libs;
402 final List<Path> configs = options.configs;
403 final List<Path> classpath = options.classpath;
404 final List<Path> headerFiles = options.headerFiles;
405 final List<Path> manPages = options.manPages;
406 final List<Path> legalNotices = options.legalNotices;
407
408 final Version moduleVersion = options.moduleVersion;
409 final String mainClass = options.mainClass;
410 final String osName = options.osName;
411 final String osArch = options.osArch;
412 final String osVersion = options.osVersion;
413 final List<PathMatcher> excludes = options.excludes;
414 final Hasher hasher = hasher();
415 final ModuleResolution moduleResolution = options.moduleResolution;
416
417 JmodFileWriter() { }
418
419 /**
420 * Writes the jmod to the given output stream.
421 */
422 void write(JmodOutputStream out) throws IOException {
423 // module-info.class
424 writeModuleInfo(out, findPackages(classpath));
425
426 // classes
427 processClasses(out, classpath);
428
429 processSection(out, Section.CONFIG, configs);
430 processSection(out, Section.HEADER_FILES, headerFiles);
431 processSection(out, Section.LEGAL_NOTICES, legalNotices);
432 processSection(out, Section.MAN_PAGES, manPages);
433 processSection(out, Section.NATIVE_CMDS, cmds);
434 processSection(out, Section.NATIVE_LIBS, libs);
497 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in);
498
499 // Add (or replace) the Packages attribute
500 if (packages != null) {
501 validatePackages(descriptor, packages);
502 extender.packages(packages);
503 }
504
505 // --main-class
506 if (mainClass != null)
507 extender.mainClass(mainClass);
508
509 // --os-name, --os-arch, --os-version
510 if (osName != null || osArch != null || osVersion != null)
511 extender.targetPlatform(osName, osArch, osVersion);
512
513 // --module-version
514 if (moduleVersion != null)
515 extender.version(moduleVersion);
516
517 if (hasher != null) {
518 ModuleHashes moduleHashes = hasher.computeHashes(descriptor.name());
519 if (moduleHashes != null) {
520 extender.hashes(moduleHashes);
521 } else {
522 warning("warn.no.module.hashes", descriptor.name());
523 }
524 }
525
526 if (moduleResolution != null && moduleResolution.value() != 0) {
527 extender.moduleResolution(moduleResolution);
528 }
529
530 // write the (possibly extended or modified) module-info.class
531 out.writeEntry(extender.toByteArray(), Section.CLASSES, MODULE_INFO);
532 }
533 }
534
535 private void validatePackages(ModuleDescriptor descriptor, Set<String> packages) {
536 Set<String> nonExistPackages = new TreeSet<>();
537 descriptor.exports().stream()
538 .map(Exports::source)
540 .forEach(nonExistPackages::add);
541
542 descriptor.opens().stream()
543 .map(Opens::source)
544 .filter(pn -> !packages.contains(pn))
545 .forEach(nonExistPackages::add);
546
547 if (!nonExistPackages.isEmpty()) {
548 throw new CommandException("err.missing.export.or.open.packages",
549 descriptor.name(), nonExistPackages);
550 }
551 }
552
553 /*
554 * Hasher resolves a module graph using the --hash-modules PATTERN
555 * as the roots.
556 *
557 * The jmod file is being created and does not exist in the
558 * given modulepath.
559 */
560 private Hasher hasher() {
561 if (options.modulesToHash == null)
562 return null;
563
564 try {
565 Supplier<InputStream> miSupplier = newModuleInfoSupplier();
566 if (miSupplier == null) {
567 throw new IOException(MODULE_INFO + " not found");
568 }
569
570 ModuleDescriptor descriptor;
571 try (InputStream in = miSupplier.get()) {
572 descriptor = ModuleDescriptor.read(in);
573 }
574
575 URI uri = options.jmodFile.toUri();
576 ModuleReference mref = new ModuleReference(descriptor, uri) {
577 @Override
578 public ModuleReader open() {
579 throw new UnsupportedOperationException();
580 }
581 };
582
583 // compose a module finder with the module path and also
584 // a module finder that can find the jmod file being created
585 ModuleFinder finder = ModuleFinder.compose(options.moduleFinder,
586 new ModuleFinder() {
587 @Override
588 public Optional<ModuleReference> find(String name) {
589 if (descriptor.name().equals(name))
590 return Optional.of(mref);
591 else return Optional.empty();
592 }
593
594 @Override
595 public Set<ModuleReference> findAll() {
596 return Collections.singleton(mref);
597 }
598 });
599
600 return new Hasher(finder);
601 } catch (IOException e) {
602 throw new UncheckedIOException(e);
603 }
604 }
605
606 /**
607 * Returns the set of all packages on the given class path.
608 */
609 Set<String> findPackages(List<Path> classpath) {
610 Set<String> packages = new HashSet<>();
611 for (Path path : classpath) {
612 if (Files.isDirectory(path)) {
613 packages.addAll(findPackages(path));
614 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) {
615 try (JarFile jf = new JarFile(path.toString())) {
616 packages.addAll(findPackages(jf));
617 } catch (ZipException x) {
618 // Skip. Do nothing. No packages will be added.
619 } catch (IOException ioe) {
620 throw new UncheckedIOException(ioe);
621 }
622 }
623 }
772 public void accept(JarEntry je) {
773 try (InputStream in = jarfile.getInputStream(je)) {
774 out.writeEntry(in, Section.CLASSES, je.getName());
775 } catch (IOException e) {
776 throw new UncheckedIOException(e);
777 }
778 }
779 @Override
780 public boolean test(JarEntry je) {
781 String name = je.getName();
782 // ## no support for excludes. Is it really needed?
783 return !name.endsWith(MODULE_INFO) && !je.isDirectory();
784 }
785 }
786 }
787
788 /**
789 * Compute and record hashes
790 */
791 private class Hasher {
792 final ModuleFinder moduleFinder;
793 final Map<String, Path> moduleNameToPath;
794 final Set<String> modules;
795 final Configuration configuration;
796 final boolean dryrun = options.dryrun;
797 Hasher(ModuleFinder finder) {
798 this.moduleFinder = finder;
799 // Determine the modules that matches the pattern {@code modulesToHash}
800 this.modules = moduleFinder.findAll().stream()
801 .map(mref -> mref.descriptor().name())
802 .filter(mn -> options.modulesToHash.matcher(mn).find())
803 .collect(Collectors.toSet());
804
805 // a map from a module name to Path of the packaged module
806 this.moduleNameToPath = moduleFinder.findAll().stream()
807 .map(mref -> mref.descriptor().name())
808 .collect(Collectors.toMap(Function.identity(), mn -> moduleToPath(mn)));
809
810 // get a resolved module graph
811 Configuration config = null;
812 try {
813 config = Configuration.empty()
814 .resolveRequires(ModuleFinder.ofSystem(), moduleFinder, modules);
815 } catch (ResolutionException e) {
816 warning("warn.module.resolution.fail", e.getMessage());
817 }
818 this.configuration = config;
819 }
820
821 /**
822 * This method is for jmod hash command.
823 *
824 * Identify the base modules in the module graph, i.e. no outgoing edge
825 * to any of the modules to be hashed.
826 *
827 * For each base module M, compute the hashes of all modules that depend
828 * upon M directly or indirectly. Then update M's module-info.class
829 * to record the hashes.
830 */
831 boolean run() {
832 if (configuration == null)
833 return false;
834
835 // transposed graph containing the the packaged modules and
836 // its transitive dependences matching --hash-modules
837 Map<String, Set<String>> graph = new HashMap<>();
838 for (String root : modules) {
839 Deque<String> deque = new ArrayDeque<>();
840 deque.add(root);
841 Set<String> visited = new HashSet<>();
842 while (!deque.isEmpty()) {
843 String mn = deque.pop();
844 if (!visited.contains(mn)) {
845 visited.add(mn);
846
847 if (modules.contains(mn))
848 graph.computeIfAbsent(mn, _k -> new HashSet<>());
849
850 ResolvedModule resolvedModule = configuration.findModule(mn).get();
851 for (ResolvedModule dm : resolvedModule.reads()) {
852 String name = dm.name();
853 if (!visited.contains(name)) {
854 deque.push(name);
855 }
856
857 // reverse edge
858 if (modules.contains(name) && modules.contains(mn)) {
859 graph.computeIfAbsent(name, _k -> new HashSet<>()).add(mn);
860 }
861 }
862 }
863 }
864 }
865
866 if (dryrun)
867 out.println("Dry run:");
868
869 // each node in a transposed graph is a matching packaged module
870 // in which the hash of the modules that depend upon it is recorded
871 graph.entrySet().stream()
872 .filter(e -> !e.getValue().isEmpty())
873 .forEach(e -> {
874 String mn = e.getKey();
875 Map<String, Path> modulesForHash = e.getValue().stream()
876 .collect(Collectors.toMap(Function.identity(),
877 moduleNameToPath::get));
878 ModuleHashes hashes = ModuleHashes.generate(modulesForHash, "SHA-256");
879 if (dryrun) {
880 out.format("%s%n", mn);
881 hashes.names().stream()
882 .sorted()
883 .forEach(name -> out.format(" hashes %s %s %s%n",
884 name, hashes.algorithm(), hashes.hashFor(name)));
885 } else {
886 try {
887 updateModuleInfo(mn, hashes);
888 } catch (IOException ex) {
889 throw new UncheckedIOException(ex);
890 }
891 }
892 });
893 return true;
894 }
895
896 /**
897 * Compute hashes of the specified module.
898 *
899 * It records the hashing modules that depend upon the specified
900 * module directly or indirectly.
901 */
902 ModuleHashes computeHashes(String name) {
903 if (configuration == null)
904 return null;
905
906 // the transposed graph includes all modules in the resolved graph
907 Map<String, Set<String>> graph = transpose();
908
909 // find the modules that transitively depend upon the specified name
910 Deque<String> deque = new ArrayDeque<>();
911 deque.add(name);
912 Set<String> mods = visitNodes(graph, deque);
913
914 // filter modules matching the pattern specified --hash-modules
915 // as well as itself as the jmod file is being generated
916 Map<String, Path> modulesForHash = mods.stream()
917 .filter(mn -> !mn.equals(name) && modules.contains(mn))
918 .collect(Collectors.toMap(Function.identity(), moduleNameToPath::get));
919
920 if (modulesForHash.isEmpty())
921 return null;
922
923 return ModuleHashes.generate(modulesForHash, "SHA-256");
924 }
925
926 /**
927 * Returns all nodes traversed from the given roots.
928 */
929 private Set<String> visitNodes(Map<String, Set<String>> graph,
930 Deque<String> roots) {
931 Set<String> visited = new HashSet<>();
932 while (!roots.isEmpty()) {
933 String mn = roots.pop();
934 if (!visited.contains(mn)) {
935 visited.add(mn);
936 // the given roots may not be part of the graph
937 if (graph.containsKey(mn)) {
938 for (String dm : graph.get(mn)) {
939 if (!visited.contains(dm)) {
940 roots.push(dm);
941 }
942 }
943 }
944 }
945 }
946 return visited;
947 }
948
949 /**
950 * Returns a transposed graph from the resolved module graph.
951 */
952 private Map<String, Set<String>> transpose() {
953 Map<String, Set<String>> transposedGraph = new HashMap<>();
954 Deque<String> deque = new ArrayDeque<>(modules);
955
956 Set<String> visited = new HashSet<>();
957 while (!deque.isEmpty()) {
958 String mn = deque.pop();
959 if (!visited.contains(mn)) {
960 visited.add(mn);
961
962 transposedGraph.computeIfAbsent(mn, _k -> new HashSet<>());
963
964 ResolvedModule resolvedModule = configuration.findModule(mn).get();
965 for (ResolvedModule dm : resolvedModule.reads()) {
966 String name = dm.name();
967 if (!visited.contains(name)) {
968 deque.push(name);
969 }
970
971 // reverse edge
972 transposedGraph.computeIfAbsent(name, _k -> new HashSet<>())
973 .add(mn);
974 }
975 }
976 }
977 return transposedGraph;
978 }
979
980 /**
981 * Reads the given input stream of module-info.class and write
982 * the extended module-info.class with the given ModuleHashes
983 *
984 * @param in InputStream of module-info.class
985 * @param out OutputStream to write the extended module-info.class
986 * @param hashes ModuleHashes
987 */
988 private void recordHashes(InputStream in, OutputStream out, ModuleHashes hashes)
989 throws IOException
990 {
991 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in);
992 extender.hashes(hashes);
993 extender.write(out);
994 }
995
996 private void updateModuleInfo(String name, ModuleHashes moduleHashes)
997 throws IOException
998 {
999 Path target = moduleNameToPath.get(name);
1000 Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp");
1001 try {
1002 if (target.getFileName().toString().endsWith(".jmod")) {
1003 updateJmodFile(target, tempTarget, moduleHashes);
1004 } else {
1005 updateModularJar(target, tempTarget, moduleHashes);
1006 }
1007 } catch (IOException|RuntimeException e) {
1008 if (Files.exists(tempTarget)) {
1009 try {
1010 Files.delete(tempTarget);
1011 } catch (IOException ioe) {
1012 e.addSuppressed(ioe);
1013 }
1014 }
1015 throw e;
1016 }
1017
1018 out.println(getMessage("module.hashes.recorded", name));
1019 Files.move(tempTarget, target, StandardCopyOption.REPLACE_EXISTING);
1020 }
1058 {
1059 jf.stream().forEach(e -> {
1060 try (InputStream in = jf.getInputStream(e.section(), e.name())) {
1061 if (e.name().equals(MODULE_INFO)) {
1062 // replace module-info.class
1063 ModuleInfoExtender extender =
1064 ModuleInfoExtender.newExtender(in);
1065 extender.hashes(moduleHashes);
1066 jos.writeEntry(extender.toByteArray(), e.section(), e.name());
1067 } else {
1068 jos.writeEntry(in, e);
1069 }
1070 } catch (IOException x) {
1071 throw new UncheckedIOException(x);
1072 }
1073 });
1074 }
1075 }
1076
1077 private Path moduleToPath(String name) {
1078 ModuleReference mref = moduleFinder.find(name).orElseThrow(
1079 () -> new InternalError("Selected module " + name + " not on module path"));
1080
1081 URI uri = mref.location().get();
1082 Path path = Paths.get(uri);
1083 String fn = path.getFileName().toString();
1084 if (!fn.endsWith(".jar") && !fn.endsWith(".jmod")) {
1085 throw new InternalError(path + " is not a modular JAR or jmod file");
1086 }
1087 return path;
1088 }
1089 }
1090
1091 /**
1092 * An abstract converter that given a string representing a list of paths,
1093 * separated by the File.pathSeparator, returns a List of java.nio.Path's.
1094 * Specific subclasses should do whatever validation is required on the
1095 * individual path elements, if any.
1096 */
1097 static abstract class AbstractPathConverter implements ValueConverter<List<Path>> {
1098 @Override
1099 public List<Path> convert(String value) {
1100 List<Path> paths = new ArrayList<>();
1101 String[] pathElements = value.split(File.pathSeparator);
|
1 /*
2 * Copyright (c) 2015, 2017, 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
41 import java.lang.module.ModuleDescriptor.Exports;
42 import java.lang.module.ModuleDescriptor.Opens;
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.FileVisitOption;
51 import java.nio.file.FileVisitResult;
52 import java.nio.file.Files;
53 import java.nio.file.InvalidPathException;
54 import java.nio.file.Path;
55 import java.nio.file.PathMatcher;
56 import java.nio.file.Paths;
57 import java.nio.file.SimpleFileVisitor;
58 import java.nio.file.StandardCopyOption;
59 import java.nio.file.attribute.BasicFileAttributes;
60 import java.text.MessageFormat;
61 import java.util.ArrayList;
62 import java.util.Collection;
63 import java.util.Collections;
64 import java.util.Comparator;
65 import java.util.HashSet;
66 import java.util.LinkedHashMap;
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.TreeSet;
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.loader.ResourceHelper;
100 import jdk.internal.module.ModuleHashes;
101 import jdk.internal.module.ModuleHashesBuilder;
102 import jdk.internal.module.ModuleInfo;
103 import jdk.internal.module.ModuleInfoExtender;
104 import jdk.internal.module.ModulePath;
105 import jdk.internal.module.ModuleResolution;
106 import jdk.tools.jlink.internal.Utils;
107
108 import static java.util.stream.Collectors.joining;
109
110 /**
111 * Implementation for the jmod tool.
112 */
113 public class JmodTask {
114
115 static class CommandException extends RuntimeException {
116 private static final long serialVersionUID = 0L;
117 boolean showUsage;
118
119 CommandException(String key, Object... args) {
120 super(getMessageOrKey(key, args));
121 }
267 int index = name.lastIndexOf("/");
268 if (index != -1) {
269 Path p = dir.resolve(name.substring(0, index));
270 if (Files.notExists(p))
271 Files.createDirectories(p);
272 }
273
274 try (OutputStream os = Files.newOutputStream(dir.resolve(name))) {
275 jf.getInputStream(e).transferTo(os);
276 }
277 } catch (IOException x) {
278 throw new UncheckedIOException(x);
279 }
280 });
281
282 return true;
283 }
284 }
285
286 private boolean hashModules() {
287 if (options.dryrun) {
288 out.println("Dry run:");
289 }
290
291 Hasher hasher = new Hasher(options.moduleFinder);
292 hasher.computeHashes().forEach((mn, hashes) -> {
293 if (options.dryrun) {
294 out.format("%s%n", mn);
295 hashes.names().stream()
296 .sorted()
297 .forEach(name -> out.format(" hashes %s %s %s%n",
298 name, hashes.algorithm(), toHex(hashes.hashFor(name))));
299 } else {
300 try {
301 hasher.updateModuleInfo(mn, hashes);
302 } catch (IOException ex) {
303 throw new UncheckedIOException(ex);
304 }
305 }
306 });
307 return true;
308 }
309
310 private boolean describe() throws IOException {
311 try (JmodFile jf = new JmodFile(options.jmodFile)) {
312 try (InputStream in = jf.getInputStream(Section.CLASSES, MODULE_INFO)) {
313 ModuleInfo.Attributes attrs = ModuleInfo.read(in, null);
314 printModuleDescriptor(attrs.descriptor(), attrs.recordedHashes());
315 return true;
316 } catch (IOException e) {
317 throw new CommandException("err.module.descriptor.not.found");
318 }
319 }
320 }
321
322 static <T> String toString(Collection<T> c) {
323 if (c.isEmpty()) { return ""; }
324 return c.stream().map(e -> e.toString().toLowerCase(Locale.ROOT))
325 .collect(joining(" "));
326 }
327
378 .append(toHex(hashes.hashFor(mod))));
379 }
380
381 out.println(sb.toString());
382 }
383
384 private String toHex(byte[] ba) {
385 StringBuilder sb = new StringBuilder(ba.length);
386 for (byte b: ba) {
387 sb.append(String.format("%02x", b & 0xff));
388 }
389 return sb.toString();
390 }
391
392 private boolean create() throws IOException {
393 JmodFileWriter jmod = new JmodFileWriter();
394
395 // create jmod with temporary name to avoid it being examined
396 // when scanning the module path
397 Path target = options.jmodFile;
398 Path tempTarget = Files.createTempFile(target.getFileName().toString(), ".tmp");
399 try {
400 try (JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) {
401 jmod.write(jos);
402 }
403 Files.move(tempTarget, target);
404 } catch (Exception e) {
405 if (Files.exists(tempTarget)) {
406 try {
407 Files.delete(tempTarget);
408 } catch (IOException ioe) {
409 e.addSuppressed(ioe);
410 }
411 }
412 throw e;
413 }
414 return true;
415 }
416
417 private class JmodFileWriter {
418 final List<Path> cmds = options.cmds;
419 final List<Path> libs = options.libs;
420 final List<Path> configs = options.configs;
421 final List<Path> classpath = options.classpath;
422 final List<Path> headerFiles = options.headerFiles;
423 final List<Path> manPages = options.manPages;
424 final List<Path> legalNotices = options.legalNotices;
425
426 final Version moduleVersion = options.moduleVersion;
427 final String mainClass = options.mainClass;
428 final String osName = options.osName;
429 final String osArch = options.osArch;
430 final String osVersion = options.osVersion;
431 final List<PathMatcher> excludes = options.excludes;
432 final ModuleResolution moduleResolution = options.moduleResolution;
433
434 JmodFileWriter() { }
435
436 /**
437 * Writes the jmod to the given output stream.
438 */
439 void write(JmodOutputStream out) throws IOException {
440 // module-info.class
441 writeModuleInfo(out, findPackages(classpath));
442
443 // classes
444 processClasses(out, classpath);
445
446 processSection(out, Section.CONFIG, configs);
447 processSection(out, Section.HEADER_FILES, headerFiles);
448 processSection(out, Section.LEGAL_NOTICES, legalNotices);
449 processSection(out, Section.MAN_PAGES, manPages);
450 processSection(out, Section.NATIVE_CMDS, cmds);
451 processSection(out, Section.NATIVE_LIBS, libs);
514 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in);
515
516 // Add (or replace) the Packages attribute
517 if (packages != null) {
518 validatePackages(descriptor, packages);
519 extender.packages(packages);
520 }
521
522 // --main-class
523 if (mainClass != null)
524 extender.mainClass(mainClass);
525
526 // --os-name, --os-arch, --os-version
527 if (osName != null || osArch != null || osVersion != null)
528 extender.targetPlatform(osName, osArch, osVersion);
529
530 // --module-version
531 if (moduleVersion != null)
532 extender.version(moduleVersion);
533
534 // --hash-modules
535 if (options.modulesToHash != null) {
536 // To compute hashes, it creates a Configuration to resolve
537 // a module graph. The post-resolution check requires
538 // the packages in ModuleDescriptor be available for validation.
539 ModuleDescriptor md;
540 try (InputStream is = miSupplier.get()) {
541 md = ModuleDescriptor.read(is, () -> packages);
542 }
543
544 ModuleHashes moduleHashes = computeHashes(md);
545 if (moduleHashes != null) {
546 extender.hashes(moduleHashes);
547 } else {
548 warning("warn.no.module.hashes", descriptor.name());
549 }
550 }
551
552 if (moduleResolution != null && moduleResolution.value() != 0) {
553 extender.moduleResolution(moduleResolution);
554 }
555
556 // write the (possibly extended or modified) module-info.class
557 out.writeEntry(extender.toByteArray(), Section.CLASSES, MODULE_INFO);
558 }
559 }
560
561 private void validatePackages(ModuleDescriptor descriptor, Set<String> packages) {
562 Set<String> nonExistPackages = new TreeSet<>();
563 descriptor.exports().stream()
564 .map(Exports::source)
566 .forEach(nonExistPackages::add);
567
568 descriptor.opens().stream()
569 .map(Opens::source)
570 .filter(pn -> !packages.contains(pn))
571 .forEach(nonExistPackages::add);
572
573 if (!nonExistPackages.isEmpty()) {
574 throw new CommandException("err.missing.export.or.open.packages",
575 descriptor.name(), nonExistPackages);
576 }
577 }
578
579 /*
580 * Hasher resolves a module graph using the --hash-modules PATTERN
581 * as the roots.
582 *
583 * The jmod file is being created and does not exist in the
584 * given modulepath.
585 */
586 private ModuleHashes computeHashes(ModuleDescriptor descriptor) {
587 String mn = descriptor.name();
588 URI uri = options.jmodFile.toUri();
589 ModuleReference mref = new ModuleReference(descriptor, uri) {
590 @Override
591 public ModuleReader open() {
592 throw new UnsupportedOperationException("opening " + mn);
593 }
594 };
595
596 // compose a module finder with the module path and also
597 // a module finder that can find the jmod file being created
598 ModuleFinder finder = ModuleFinder.compose(options.moduleFinder,
599 new ModuleFinder() {
600 @Override
601 public Optional<ModuleReference> find(String name) {
602 if (descriptor.name().equals(name))
603 return Optional.of(mref);
604 else return Optional.empty();
605 }
606
607 @Override
608 public Set<ModuleReference> findAll() {
609 return Collections.singleton(mref);
610 }
611 });
612
613 return new Hasher(mn, finder).computeHashes().get(mn);
614 }
615
616 /**
617 * Returns the set of all packages on the given class path.
618 */
619 Set<String> findPackages(List<Path> classpath) {
620 Set<String> packages = new HashSet<>();
621 for (Path path : classpath) {
622 if (Files.isDirectory(path)) {
623 packages.addAll(findPackages(path));
624 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) {
625 try (JarFile jf = new JarFile(path.toString())) {
626 packages.addAll(findPackages(jf));
627 } catch (ZipException x) {
628 // Skip. Do nothing. No packages will be added.
629 } catch (IOException ioe) {
630 throw new UncheckedIOException(ioe);
631 }
632 }
633 }
782 public void accept(JarEntry je) {
783 try (InputStream in = jarfile.getInputStream(je)) {
784 out.writeEntry(in, Section.CLASSES, je.getName());
785 } catch (IOException e) {
786 throw new UncheckedIOException(e);
787 }
788 }
789 @Override
790 public boolean test(JarEntry je) {
791 String name = je.getName();
792 // ## no support for excludes. Is it really needed?
793 return !name.endsWith(MODULE_INFO) && !je.isDirectory();
794 }
795 }
796 }
797
798 /**
799 * Compute and record hashes
800 */
801 private class Hasher {
802 final Configuration configuration;
803 final ModuleHashesBuilder hashesBuilder;
804 final Set<String> modules;
805 final String moduleName; // a specific module to record hashes, if set
806
807 /**
808 * This constructor is for jmod hash command.
809 *
810 * This Hasher will determine which modules to record hashes, i.e.
811 * the module in a subgraph of modules to be hashed and that
812 * has no outgoing edges. It will record in each of these modules,
813 * say `M`, with the the hashes of modules that depend upon M
814 * directly or indirectly matching the specified --hash-modules pattern.
815 */
816 Hasher(ModuleFinder finder) {
817 this(null, finder);
818 }
819
820 /**
821 * Constructs a Hasher to compute hashes.
822 *
823 * If a module name `M` is specified, it will compute the hashes of
824 * modules that depend upon M directly or indirectly matching the
825 * specified --hash-modules pattern and record in the ModuleHashes
826 * attribute in M's module-info.class.
827 *
828 * @param name name of the module to record hashes
829 * @param finder module finder for the specified --module-path
830 */
831 Hasher(String name, ModuleFinder finder) {
832 // Determine the modules that matches the pattern {@code modulesToHash}
833 Set<String> roots = finder.findAll().stream()
834 .map(mref -> mref.descriptor().name())
835 .filter(mn -> options.modulesToHash.matcher(mn).find())
836 .collect(Collectors.toSet());
837
838 // use system module path unless it creates a JMOD file for
839 // a module that is present in the system image e.g. upgradeable
840 // module
841 ModuleFinder system;
842 if (name != null && ModuleFinder.ofSystem().find(name).isPresent()) {
843 system = ModuleFinder.of();
844 } else {
845 system = ModuleFinder.ofSystem();
846 }
847 // get a resolved module graph
848 Configuration config = null;
849 try {
850 config = Configuration.empty().resolveRequires(system, finder, roots);
851 } catch (ResolutionException e) {
852 throw new CommandException("err.module.resolution.fail", e.getMessage());
853 }
854
855 this.moduleName = name;
856 this.configuration = config;
857
858 // filter modules resolved from the system module finder
859 this.modules = config.modules().stream()
860 .map(ResolvedModule::name)
861 .filter(mn -> roots.contains(mn) && !system.find(mn).isPresent())
862 .collect(Collectors.toSet());
863
864 this.hashesBuilder = new ModuleHashesBuilder(config, modules);
865 }
866
867 /**
868 * Returns a map of a module M to record hashes of the modules
869 * that depend upon M directly or indirectly.
870 *
871 * For jmod hash command, the returned map contains one entry
872 * for each module M that has no outgoing edges to any of the
873 * modules matching the specified --hash-modules pattern.
874 *
875 * Each entry represents a leaf node in a connected subgraph containing
876 * M and other candidate modules from the module graph where M's outgoing
877 * edges to any module other than the ones matching the specified
878 * --hash-modules pattern are excluded.
879 */
880 Map<String, ModuleHashes> computeHashes() {
881 if (hashesBuilder == null)
882 return null;
883
884 if (moduleName != null) {
885 return hashesBuilder.computeHashes(Set.of(moduleName));
886 } else {
887 return hashesBuilder.computeHashes(modules);
888 }
889 }
890
891 /**
892 * Reads the given input stream of module-info.class and write
893 * the extended module-info.class with the given ModuleHashes
894 *
895 * @param in InputStream of module-info.class
896 * @param out OutputStream to write the extended module-info.class
897 * @param hashes ModuleHashes
898 */
899 private void recordHashes(InputStream in, OutputStream out, ModuleHashes hashes)
900 throws IOException
901 {
902 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in);
903 extender.hashes(hashes);
904 extender.write(out);
905 }
906
907 void updateModuleInfo(String name, ModuleHashes moduleHashes)
908 throws IOException
909 {
910 Path target = moduleToPath(name);
911 Path tempTarget = Files.createTempFile(target.getFileName().toString(), ".tmp");
912 try {
913 if (target.getFileName().toString().endsWith(".jmod")) {
914 updateJmodFile(target, tempTarget, moduleHashes);
915 } else {
916 updateModularJar(target, tempTarget, moduleHashes);
917 }
918 } catch (IOException|RuntimeException e) {
919 if (Files.exists(tempTarget)) {
920 try {
921 Files.delete(tempTarget);
922 } catch (IOException ioe) {
923 e.addSuppressed(ioe);
924 }
925 }
926 throw e;
927 }
928
929 out.println(getMessage("module.hashes.recorded", name));
930 Files.move(tempTarget, target, StandardCopyOption.REPLACE_EXISTING);
931 }
969 {
970 jf.stream().forEach(e -> {
971 try (InputStream in = jf.getInputStream(e.section(), e.name())) {
972 if (e.name().equals(MODULE_INFO)) {
973 // replace module-info.class
974 ModuleInfoExtender extender =
975 ModuleInfoExtender.newExtender(in);
976 extender.hashes(moduleHashes);
977 jos.writeEntry(extender.toByteArray(), e.section(), e.name());
978 } else {
979 jos.writeEntry(in, e);
980 }
981 } catch (IOException x) {
982 throw new UncheckedIOException(x);
983 }
984 });
985 }
986 }
987
988 private Path moduleToPath(String name) {
989 ResolvedModule rm = configuration.findModule(name).orElseThrow(
990 () -> new InternalError("Selected module " + name + " not on module path"));
991
992 URI uri = rm.reference().location().get();
993 Path path = Paths.get(uri);
994 String fn = path.getFileName().toString();
995 if (!fn.endsWith(".jar") && !fn.endsWith(".jmod")) {
996 throw new InternalError(path + " is not a modular JAR or jmod file");
997 }
998 return path;
999 }
1000 }
1001
1002 /**
1003 * An abstract converter that given a string representing a list of paths,
1004 * separated by the File.pathSeparator, returns a List of java.nio.Path's.
1005 * Specific subclasses should do whatever validation is required on the
1006 * individual path elements, if any.
1007 */
1008 static abstract class AbstractPathConverter implements ValueConverter<List<Path>> {
1009 @Override
1010 public List<Path> convert(String value) {
1011 List<Path> paths = new ArrayList<>();
1012 String[] pathElements = value.split(File.pathSeparator);
|