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 tmpdir = Paths.get(System.getProperty("java.io.tmpdir"));
399 Path tempTarget = tmpdir.resolve(target.getFileName().toString() + ".tmp");
400 try {
401 try (JmodOutputStream jos = JmodOutputStream.newOutputStream(tempTarget)) {
402 jmod.write(jos);
403 }
404 Files.move(tempTarget, target);
405 } catch (Exception e) {
406 if (Files.exists(tempTarget)) {
407 try {
408 Files.delete(tempTarget);
409 } catch (IOException ioe) {
410 e.addSuppressed(ioe);
411 }
412 }
413 throw e;
414 }
415 return true;
416 }
417
418 private class JmodFileWriter {
419 final List<Path> cmds = options.cmds;
420 final List<Path> libs = options.libs;
421 final List<Path> configs = options.configs;
422 final List<Path> classpath = options.classpath;
423 final List<Path> headerFiles = options.headerFiles;
424 final List<Path> manPages = options.manPages;
425 final List<Path> legalNotices = options.legalNotices;
426
427 final Version moduleVersion = options.moduleVersion;
428 final String mainClass = options.mainClass;
429 final String osName = options.osName;
430 final String osArch = options.osArch;
431 final String osVersion = options.osVersion;
432 final List<PathMatcher> excludes = options.excludes;
433 final ModuleResolution moduleResolution = options.moduleResolution;
434
435 JmodFileWriter() { }
436
437 /**
438 * Writes the jmod to the given output stream.
439 */
440 void write(JmodOutputStream out) throws IOException {
441 // module-info.class
442 writeModuleInfo(out, findPackages(classpath));
443
444 // classes
445 processClasses(out, classpath);
446
447 processSection(out, Section.CONFIG, configs);
448 processSection(out, Section.HEADER_FILES, headerFiles);
449 processSection(out, Section.LEGAL_NOTICES, legalNotices);
450 processSection(out, Section.MAN_PAGES, manPages);
451 processSection(out, Section.NATIVE_CMDS, cmds);
452 processSection(out, Section.NATIVE_LIBS, libs);
515 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in);
516
517 // Add (or replace) the Packages attribute
518 if (packages != null) {
519 validatePackages(descriptor, packages);
520 extender.packages(packages);
521 }
522
523 // --main-class
524 if (mainClass != null)
525 extender.mainClass(mainClass);
526
527 // --os-name, --os-arch, --os-version
528 if (osName != null || osArch != null || osVersion != null)
529 extender.targetPlatform(osName, osArch, osVersion);
530
531 // --module-version
532 if (moduleVersion != null)
533 extender.version(moduleVersion);
534
535 // --hash-modules
536 if (options.modulesToHash != null) {
537 // To compute hashes, it creates a Configuration to resolve
538 // a module graph. The post-resolution check requires
539 // the packages in ModuleDescriptor be available for validation.
540 ModuleDescriptor md;
541 try (InputStream is = miSupplier.get()) {
542 md = ModuleDescriptor.read(is, () -> packages);
543 }
544
545 ModuleHashes moduleHashes = computeHashes(md);
546 if (moduleHashes != null) {
547 extender.hashes(moduleHashes);
548 } else {
549 warning("warn.no.module.hashes", descriptor.name());
550 }
551 }
552
553 if (moduleResolution != null && moduleResolution.value() != 0) {
554 extender.moduleResolution(moduleResolution);
555 }
556
557 // write the (possibly extended or modified) module-info.class
558 out.writeEntry(extender.toByteArray(), Section.CLASSES, MODULE_INFO);
559 }
560 }
561
562 private void validatePackages(ModuleDescriptor descriptor, Set<String> packages) {
563 Set<String> nonExistPackages = new TreeSet<>();
564 descriptor.exports().stream()
565 .map(Exports::source)
567 .forEach(nonExistPackages::add);
568
569 descriptor.opens().stream()
570 .map(Opens::source)
571 .filter(pn -> !packages.contains(pn))
572 .forEach(nonExistPackages::add);
573
574 if (!nonExistPackages.isEmpty()) {
575 throw new CommandException("err.missing.export.or.open.packages",
576 descriptor.name(), nonExistPackages);
577 }
578 }
579
580 /*
581 * Hasher resolves a module graph using the --hash-modules PATTERN
582 * as the roots.
583 *
584 * The jmod file is being created and does not exist in the
585 * given modulepath.
586 */
587 private ModuleHashes computeHashes(ModuleDescriptor descriptor) {
588 String mn = descriptor.name();
589 URI uri = options.jmodFile.toUri();
590 ModuleReference mref = new ModuleReference(descriptor, uri) {
591 @Override
592 public ModuleReader open() {
593 throw new UnsupportedOperationException("opening " + mn);
594 }
595 };
596
597 // compose a module finder with the module path and also
598 // a module finder that can find the jmod file being created
599 ModuleFinder finder = ModuleFinder.compose(options.moduleFinder,
600 new ModuleFinder() {
601 @Override
602 public Optional<ModuleReference> find(String name) {
603 if (descriptor.name().equals(name))
604 return Optional.of(mref);
605 else return Optional.empty();
606 }
607
608 @Override
609 public Set<ModuleReference> findAll() {
610 return Collections.singleton(mref);
611 }
612 });
613
614 return new Hasher(mn, finder).computeHashes().get(mn);
615 }
616
617 /**
618 * Returns the set of all packages on the given class path.
619 */
620 Set<String> findPackages(List<Path> classpath) {
621 Set<String> packages = new HashSet<>();
622 for (Path path : classpath) {
623 if (Files.isDirectory(path)) {
624 packages.addAll(findPackages(path));
625 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) {
626 try (JarFile jf = new JarFile(path.toString())) {
627 packages.addAll(findPackages(jf));
628 } catch (ZipException x) {
629 // Skip. Do nothing. No packages will be added.
630 } catch (IOException ioe) {
631 throw new UncheckedIOException(ioe);
632 }
633 }
634 }
783 public void accept(JarEntry je) {
784 try (InputStream in = jarfile.getInputStream(je)) {
785 out.writeEntry(in, Section.CLASSES, je.getName());
786 } catch (IOException e) {
787 throw new UncheckedIOException(e);
788 }
789 }
790 @Override
791 public boolean test(JarEntry je) {
792 String name = je.getName();
793 // ## no support for excludes. Is it really needed?
794 return !name.endsWith(MODULE_INFO) && !je.isDirectory();
795 }
796 }
797 }
798
799 /**
800 * Compute and record hashes
801 */
802 private class Hasher {
803 final Configuration configuration;
804 final ModuleHashesBuilder hashesBuilder;
805 final Set<String> modules;
806 final Optional<String> moduleName; // a specific module to record hashes
807
808 /**
809 * This constructor is for jmod hash command.
810 *
811 * This Hasher will determine which modules to record hashes, i.e.
812 * the module in a subgraph of modules to be hashed and that
813 * has no outgoing edges. It will record in each of these modules,
814 * say `M`, with the the hashes of modules that depend upon M
815 * directly or indirectly matching the specified --hash-modules pattern.
816 */
817 Hasher(ModuleFinder finder) {
818 this(null, finder);
819 }
820
821 /**
822 * Constructs a Hasher to compute hashes.
823 *
824 * If a module name `M` is specified, it will compute the hashes of
825 * modules that depend upon M directly or indirectly matching the
826 * specified --hash-modules pattern and record in the ModuleHashes
827 * attribute in M's module-info.class.
828 *
829 * @param name name of he module to record hashes
830 * @param finder module finder for the specified --module-path
831 */
832 Hasher(String name, ModuleFinder finder) {
833 this.moduleName = Optional.ofNullable(name);
834
835 // Determine the modules that matches the pattern {@code modulesToHash}
836 Set<String> roots = finder.findAll().stream()
837 .map(mref -> mref.descriptor().name())
838 .filter(mn -> options.modulesToHash.matcher(mn).find())
839 .collect(Collectors.toSet());
840
841 // use system module path unless it creates a JMOD file for
842 // a module that is present in the system image e.g. upgradeable
843 // module
844 ModuleFinder system;
845 if (name != null && ModuleFinder.ofSystem().find(name).isPresent()) {
846 system = ModuleFinder.of();
847 } else {
848 system = ModuleFinder.ofSystem();
849 }
850 // get a resolved module graph
851 Configuration config = null;
852 try {
853 config = Configuration.empty().resolveRequires(system, finder, roots);
854 } catch (ResolutionException e) {
855 throw new CommandException("err.module.resolution.fail", e.getMessage());
856 }
857 this.configuration = config;
858
859 // filter modules resolved from the system module finder
860 this.modules = config.modules().stream()
861 .map(ResolvedModule::name)
862 .filter(mn -> roots.contains(mn) && !system.find(mn).isPresent())
863 .collect(Collectors.toSet());
864
865 this.hashesBuilder = new ModuleHashesBuilder(config, modules);
866 }
867
868 /**
869 * Returns a map of a module M to record hashes of the modules
870 * that depend upon M directly or indirectly.
871 *
872 * For jmod hash command, each entry in the returned map is a module
873 * in a subgraph containing the modules specified in the --hash-modules
874 * pattern but it has no outgoing edges.
875 */
876 Map<String, ModuleHashes> computeHashes() {
877 if (hashesBuilder == null)
878 return null;
879
880 if (moduleName.isPresent()) {
881 return hashesBuilder.computeHashes(Set.of(moduleName.get()));
882 } else {
883 return hashesBuilder.computeHashes(modules);
884 }
885 }
886
887 /**
888 * Reads the given input stream of module-info.class and write
889 * the extended module-info.class with the given ModuleHashes
890 *
891 * @param in InputStream of module-info.class
892 * @param out OutputStream to write the extended module-info.class
893 * @param hashes ModuleHashes
894 */
895 private void recordHashes(InputStream in, OutputStream out, ModuleHashes hashes)
896 throws IOException
897 {
898 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in);
899 extender.hashes(hashes);
900 extender.write(out);
901 }
902
903 void updateModuleInfo(String name, ModuleHashes moduleHashes)
904 throws IOException
905 {
906 Path target = moduleToPath(name);
907 Path tmpdir = Paths.get(System.getProperty("java.io.tmpdir"));
908 Path tempTarget = tmpdir.resolve(target.getFileName().toString() + ".tmp");
909 try {
910 if (target.getFileName().toString().endsWith(".jmod")) {
911 updateJmodFile(target, tempTarget, moduleHashes);
912 } else {
913 updateModularJar(target, tempTarget, moduleHashes);
914 }
915 } catch (IOException|RuntimeException e) {
916 if (Files.exists(tempTarget)) {
917 try {
918 Files.delete(tempTarget);
919 } catch (IOException ioe) {
920 e.addSuppressed(ioe);
921 }
922 }
923 throw e;
924 }
925
926 out.println(getMessage("module.hashes.recorded", name));
927 Files.move(tempTarget, target, StandardCopyOption.REPLACE_EXISTING);
928 }
966 {
967 jf.stream().forEach(e -> {
968 try (InputStream in = jf.getInputStream(e.section(), e.name())) {
969 if (e.name().equals(MODULE_INFO)) {
970 // replace module-info.class
971 ModuleInfoExtender extender =
972 ModuleInfoExtender.newExtender(in);
973 extender.hashes(moduleHashes);
974 jos.writeEntry(extender.toByteArray(), e.section(), e.name());
975 } else {
976 jos.writeEntry(in, e);
977 }
978 } catch (IOException x) {
979 throw new UncheckedIOException(x);
980 }
981 });
982 }
983 }
984
985 private Path moduleToPath(String name) {
986 ResolvedModule rm = configuration.findModule(name).orElseThrow(
987 () -> new InternalError("Selected module " + name + " not on module path"));
988
989 URI uri = rm.reference().location().get();
990 Path path = Paths.get(uri);
991 String fn = path.getFileName().toString();
992 if (!fn.endsWith(".jar") && !fn.endsWith(".jmod")) {
993 throw new InternalError(path + " is not a modular JAR or jmod file");
994 }
995 return path;
996 }
997 }
998
999 /**
1000 * An abstract converter that given a string representing a list of paths,
1001 * separated by the File.pathSeparator, returns a List of java.nio.Path's.
1002 * Specific subclasses should do whatever validation is required on the
1003 * individual path elements, if any.
1004 */
1005 static abstract class AbstractPathConverter implements ValueConverter<List<Path>> {
1006 @Override
1007 public List<Path> convert(String value) {
1008 List<Path> paths = new ArrayList<>();
1009 String[] pathElements = value.split(File.pathSeparator);
|