1 /*
2 * Copyright (c) 2015, 2024, 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 package jdk.tools.jlink.internal;
26
27 import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE;
28
29 import java.io.BufferedInputStream;
30 import java.io.File;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.PrintWriter;
34 import java.io.UncheckedIOException;
35 import java.lang.module.Configuration;
36 import java.lang.module.FindException;
37 import java.lang.module.ModuleDescriptor;
38 import java.lang.module.ModuleFinder;
39 import java.lang.module.ModuleReference;
40 import java.lang.module.ResolutionException;
41 import java.lang.module.ResolvedModule;
42 import java.net.URI;
43 import java.nio.ByteOrder;
44 import java.nio.file.FileVisitResult;
45 import java.nio.file.Files;
46 import java.nio.file.Path;
47 import java.nio.file.Paths;
48 import java.nio.file.SimpleFileVisitor;
49 import java.nio.file.attribute.BasicFileAttributes;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Comparator;
53 import java.util.Date;
54 import java.util.HashMap;
55 import java.util.HashSet;
56 import java.util.List;
57 import java.util.Locale;
58 import java.util.Map;
59 import java.util.Objects;
60 import java.util.Optional;
61 import java.util.Set;
62 import java.util.stream.Collectors;
63 import java.util.stream.Stream;
64
65 import jdk.internal.module.ModuleBootstrap;
66 import jdk.internal.module.ModulePath;
67 import jdk.internal.module.ModuleReferenceImpl;
68 import jdk.internal.module.ModuleResolution;
69 import jdk.internal.opt.CommandLine;
70 import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider;
71 import jdk.tools.jlink.internal.Jlink.JlinkConfiguration;
72 import jdk.tools.jlink.internal.Jlink.PluginsConfiguration;
73 import jdk.tools.jlink.internal.TaskHelper.BadArgs;
74 import jdk.tools.jlink.internal.TaskHelper.Option;
75 import jdk.tools.jlink.internal.TaskHelper.OptionsHelper;
76 import jdk.tools.jlink.plugin.PluginException;
77
78 /**
79 * Implementation for the jlink tool.
80 *
81 * ## Should use jdk.joptsimple some day.
82 */
83 public class JlinkTask {
84 public static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
85
86 // jlink API ignores by default. Remove when signing is implemented.
87 static final boolean IGNORE_SIGNING_DEFAULT = true;
88
89 private static final TaskHelper taskHelper
90 = new TaskHelper(JLINK_BUNDLE);
91 private static final Option<?>[] recognizedOptions = {
92 new Option<JlinkTask>(false, (task, opt, arg) -> {
93 task.options.help = true;
94 }, "--help", "-h", "-?"),
95 new Option<JlinkTask>(true, (task, opt, arg) -> {
96 // if used multiple times, the last one wins!
97 // So, clear previous values, if any.
98 task.options.modulePath.clear();
99 String[] dirs = arg.split(File.pathSeparator);
100 Arrays.stream(dirs)
101 .map(Paths::get)
102 .forEach(task.options.modulePath::add);
103 }, "--module-path", "-p"),
104 new Option<JlinkTask>(true, (task, opt, arg) -> {
105 // if used multiple times, the last one wins!
106 // So, clear previous values, if any.
107 task.options.limitMods.clear();
108 for (String mn : arg.split(",")) {
109 if (mn.isEmpty()) {
110 throw taskHelper.newBadArgs("err.mods.must.be.specified",
111 "--limit-modules");
112 }
113 task.options.limitMods.add(mn);
114 }
115 }, "--limit-modules"),
116 new Option<JlinkTask>(true, (task, opt, arg) -> {
117 for (String mn : arg.split(",")) {
118 if (mn.isEmpty()) {
119 throw taskHelper.newBadArgs("err.mods.must.be.specified",
120 "--add-modules");
121 }
122 task.options.addMods.add(mn);
123 }
124 }, "--add-modules"),
125 new Option<JlinkTask>(true, (task, opt, arg) -> {
126 Path path = Paths.get(arg);
127 task.options.output = path;
128 }, "--output"),
129 new Option<JlinkTask>(false, (task, opt, arg) -> {
130 task.options.bindServices = true;
131 }, "--bind-services"),
132 new Option<JlinkTask>(false, (task, opt, arg) -> {
133 task.options.suggestProviders = true;
134 }, "--suggest-providers", "", true),
135 new Option<JlinkTask>(true, (task, opt, arg) -> {
136 String[] values = arg.split("=");
137 // check values
138 if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) {
139 throw taskHelper.newBadArgs("err.launcher.value.format", arg);
140 } else {
141 String commandName = values[0];
142 String moduleAndMain = values[1];
143 int idx = moduleAndMain.indexOf("/");
144 if (idx != -1) {
145 if (moduleAndMain.substring(0, idx).isEmpty()) {
146 throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg);
147 }
148
149 if (moduleAndMain.substring(idx + 1).isEmpty()) {
150 throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg);
151 }
152 }
153 task.options.launchers.put(commandName, moduleAndMain);
154 }
155 }, "--launcher"),
156 new Option<JlinkTask>(true, (task, opt, arg) -> {
157 if ("little".equals(arg)) {
158 task.options.endian = ByteOrder.LITTLE_ENDIAN;
159 } else if ("big".equals(arg)) {
160 task.options.endian = ByteOrder.BIG_ENDIAN;
161 } else {
162 throw taskHelper.newBadArgs("err.unknown.byte.order", arg);
163 }
164 }, "--endian"),
165 new Option<JlinkTask>(false, (task, opt, arg) -> {
166 task.options.verbose = true;
167 }, "--verbose", "-v"),
168 new Option<JlinkTask>(false, (task, opt, arg) -> {
169 task.options.version = true;
170 }, "--version"),
171 new Option<JlinkTask>(true, (task, opt, arg) -> {
172 Path path = Paths.get(arg);
173 if (Files.exists(path)) {
174 throw taskHelper.newBadArgs("err.dir.exists", path);
175 }
176 task.options.packagedModulesPath = path;
177 }, true, "--keep-packaged-modules"),
178 new Option<JlinkTask>(true, (task, opt, arg) -> {
179 task.options.saveoptsfile = arg;
180 }, "--save-opts"),
181 new Option<JlinkTask>(false, (task, opt, arg) -> {
182 task.options.fullVersion = true;
183 }, true, "--full-version"),
184 new Option<JlinkTask>(false, (task, opt, arg) -> {
185 task.options.ignoreSigning = true;
186 }, "--ignore-signing-information"),
187 new Option<JlinkTask>(false, (task, opt, arg) -> {
188 task.options.ignoreModifiedRuntime = true;
189 }, true, "--ignore-modified-runtime"),
190 // option for generating a runtime that can then
191 // be used for linking from the run-time image.
192 new Option<JlinkTask>(false, (task, opt, arg) -> {
193 task.options.generateLinkableRuntime = true;
194 }, true, "--generate-linkable-runtime")
195 };
196
197
198 private static final String PROGNAME = "jlink";
199 private final OptionsValues options = new OptionsValues();
200
201 private static final OptionsHelper<JlinkTask> optionsHelper
202 = taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions);
203 private PrintWriter log;
204
205 void setLog(PrintWriter out, PrintWriter err) {
206 log = out;
207 taskHelper.setLog(log);
208 }
209
210 /**
211 * Result codes.
212 */
213 static final int
214 EXIT_OK = 0, // Completed with no errors.
215 EXIT_ERROR = 1, // Completed but reported errors.
216 EXIT_CMDERR = 2, // Bad command-line arguments
217 EXIT_SYSERR = 3, // System error or resource exhaustion.
218 EXIT_ABNORMAL = 4;// terminated abnormally
219
220 static class OptionsValues {
221 boolean help;
222 String saveoptsfile;
223 boolean verbose;
224 boolean version;
225 boolean fullVersion;
226 final List<Path> modulePath = new ArrayList<>();
227 final Set<String> limitMods = new HashSet<>();
228 final Set<String> addMods = new HashSet<>();
229 Path output;
230 final Map<String, String> launchers = new HashMap<>();
231 Path packagedModulesPath;
232 ByteOrder endian;
233 boolean ignoreSigning = false;
234 boolean bindServices = false;
235 boolean suggestProviders = false;
236 boolean ignoreModifiedRuntime = false;
237 boolean generateLinkableRuntime = false;
238 }
239
240 public static final String OPTIONS_RESOURCE = "jdk/tools/jlink/internal/options";
241
242 int run(String[] args) {
243 if (log == null) {
244 setLog(new PrintWriter(System.out, true),
245 new PrintWriter(System.err, true));
246 }
247 Path outputPath = null;
248 try {
249 Module m = JlinkTask.class.getModule();
250 try (InputStream savedOptions = m.getResourceAsStream(OPTIONS_RESOURCE)) {
251 if (savedOptions != null) {
252 List<String> prependArgs = new ArrayList<>();
253 CommandLine.loadCmdFile(savedOptions, prependArgs);
254 if (!prependArgs.isEmpty()) {
255 prependArgs.addAll(Arrays.asList(args));
256 args = prependArgs.toArray(new String[prependArgs.size()]);
257 }
258 }
259 }
260
261 List<String> remaining = optionsHelper.handleOptions(this, args);
262 if (remaining.size() > 0 && !options.suggestProviders) {
263 throw taskHelper.newBadArgs("err.orphan.arguments",
264 remaining.stream().collect(Collectors.joining(" ")))
265 .showUsage(true);
266 }
267 if (options.help) {
268 optionsHelper.showHelp(PROGNAME, LinkableRuntimeImage.isLinkableRuntime());
269 return EXIT_OK;
270 }
271 if (optionsHelper.shouldListPlugins()) {
272 optionsHelper.listPlugins();
273 return EXIT_OK;
274 }
275 if (options.version || options.fullVersion) {
276 taskHelper.showVersion(options.fullVersion);
277 return EXIT_OK;
278 }
279
280 JlinkConfiguration config = initJlinkConfig();
281 outputPath = config.getOutput();
282 if (options.suggestProviders) {
283 suggestProviders(config, remaining);
284 } else {
285 createImage(config);
286 if (options.saveoptsfile != null) {
287 Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes());
288 }
289 }
290
291 return EXIT_OK;
292 } catch (FindException e) {
293 log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
294 e.printStackTrace(log);
295 return EXIT_ERROR;
296 } catch (PluginException | UncheckedIOException | IOException e) {
297 log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
298 if (DEBUG) {
299 e.printStackTrace(log);
300 }
301 cleanupOutput(outputPath);
302 return EXIT_ERROR;
303 } catch (IllegalArgumentException | ResolutionException e) {
304 log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
305 if (DEBUG) {
306 e.printStackTrace(log);
307 }
308 return EXIT_ERROR;
309 } catch (BadArgs e) {
310 taskHelper.reportError(e.key, e.args);
311 if (e.showUsage) {
312 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME));
313 }
314 if (DEBUG) {
315 e.printStackTrace(log);
316 }
317 return EXIT_CMDERR;
318 } catch (Throwable x) {
319 log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage());
320 x.printStackTrace(log);
321 cleanupOutput(outputPath);
322 return EXIT_ABNORMAL;
323 } finally {
324 log.flush();
325 }
326 }
327
328 private void cleanupOutput(Path dir) {
329 try {
330 if (dir != null && Files.isDirectory(dir)) {
331 deleteDirectory(dir);
332 }
333 } catch (IOException io) {
334 log.println(taskHelper.getMessage("error.prefix") + " " + io.getMessage());
335 if (DEBUG) {
336 io.printStackTrace(log);
337 }
338 }
339 }
340
341 /*
342 * Jlink API entry point.
343 */
344 public static void createImage(JlinkConfiguration config,
345 PluginsConfiguration plugins)
346 throws Exception {
347 Objects.requireNonNull(config);
348 Objects.requireNonNull(config.getOutput());
349 plugins = plugins == null ? new PluginsConfiguration() : plugins;
350
351 // First create the image provider
352 ImageProvider imageProvider =
353 createImageProvider(config,
354 null,
355 IGNORE_SIGNING_DEFAULT,
356 false,
357 null,
358 false,
359 new OptionsValues(),
360 null);
361
362 // Then create the Plugin Stack
363 ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
364
365 //Ask the stack to proceed;
366 stack.operate(imageProvider);
367 }
368
369 // the token for "all modules on the module path"
370 private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
371 private JlinkConfiguration initJlinkConfig() throws BadArgs {
372 // Empty module path not allowed with ALL-MODULE-PATH in --add-modules
373 if (options.addMods.contains(ALL_MODULE_PATH) && options.modulePath.isEmpty()) {
374 throw taskHelper.newBadArgs("err.no.module.path");
375 }
376 ModuleFinder appModuleFinder = newModuleFinder(options.modulePath);
377 ModuleFinder finder = appModuleFinder;
378
379 boolean isLinkFromRuntime = false;
380 if (!appModuleFinder.find("java.base").isPresent()) {
381 // If the application module finder doesn't contain the
382 // java.base module we have one of two cases:
383 // 1. A custom module is being linked into a runtime, but the JDK
384 // modules have not been provided on the module path.
385 // 2. We have a run-time image based link.
386 //
387 // Distinguish case 2 by adding the default 'jmods' folder and try
388 // the look-up again. For case 1 this will now find java.base, but
389 // not for case 2, since the jmods folder is not there or doesn't
390 // include the java.base module.
391 Path defModPath = getDefaultModulePath();
392 if (defModPath != null) {
393 List<Path> combinedPaths = new ArrayList<>(options.modulePath);
394 combinedPaths.add(defModPath);
395 finder = newModuleFinder(combinedPaths);
396 }
397 // We've just added the default module path ('jmods'). If we still
398 // don't find java.base, we must resolve JDK modules from the
399 // current run-time image.
400 if (!finder.find("java.base").isPresent()) {
401 // If we don't have a linkable run-time image this is an error
402 if (!LinkableRuntimeImage.isLinkableRuntime()) {
403 throw taskHelper.newBadArgs("err.runtime.link.not.linkable.runtime");
404 }
405 isLinkFromRuntime = true;
406 // JDK modules come from the system module path
407 finder = ModuleFinder.compose(ModuleFinder.ofSystem(), appModuleFinder);
408 }
409 }
410
411 // Sanity check version if we use JMODs
412 if (!isLinkFromRuntime) {
413 checkJavaBaseVersion(finder);
414 }
415
416 // Determine the roots set
417 Set<String> roots = new HashSet<>();
418 for (String mod : options.addMods) {
419 if (mod.equals(ALL_MODULE_PATH)) {
420 // Using --limit-modules with ALL-MODULE-PATH is an error
421 if (!options.limitMods.isEmpty()) {
422 throw taskHelper.newBadArgs("err.limit.modules");
423 }
424 // all observable modules in the app module path are roots
425 Set<String> initialRoots = appModuleFinder.findAll()
426 .stream()
427 .map(ModuleReference::descriptor)
428 .map(ModuleDescriptor::name)
429 .collect(Collectors.toSet());
430
431 // Error if no module is found on the app module path
432 if (initialRoots.isEmpty()) {
433 String modPath = options.modulePath.stream()
434 .map(a -> a.toString())
435 .collect(Collectors.joining(", "));
436 throw taskHelper.newBadArgs("err.empty.module.path", modPath);
437 }
438
439 // Use a module finder with limited observability, as determined
440 // by initialRoots, to find the observable modules from the
441 // application module path (--module-path option) only. We must
442 // not include JDK modules from the default module path or the
443 // run-time image.
444 ModuleFinder mf = limitFinder(finder, initialRoots, Set.of());
445 mf.findAll()
446 .stream()
447 .map(ModuleReference::descriptor)
448 .map(ModuleDescriptor::name)
449 .forEach(mn -> roots.add(mn));
450 } else {
451 roots.add(mod);
452 }
453 }
454 finder = limitFinder(finder, options.limitMods, roots);
455
456 // --keep-packaged-modules doesn't make sense as we are not linking
457 // from packaged modules to begin with.
458 if (isLinkFromRuntime && options.packagedModulesPath != null) {
459 throw taskHelper.newBadArgs("err.runtime.link.packaged.mods");
460 }
461
462 return new JlinkConfiguration(options.output,
463 roots,
464 finder,
465 isLinkFromRuntime,
466 options.ignoreModifiedRuntime,
467 options.generateLinkableRuntime);
468 }
469
470 /*
471 * Creates a ModuleFinder for the given module paths.
472 */
473 public static ModuleFinder newModuleFinder(List<Path> paths) {
474 Runtime.Version version = Runtime.version();
475 Path[] entries = paths.toArray(new Path[0]);
476 return ModulePath.of(version, true, entries);
477 }
478
479 private void createImage(JlinkConfiguration config) throws Exception {
480 if (options.output == null) {
481 throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true);
482 }
483 if (options.addMods.isEmpty()) {
484 throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules")
485 .showUsage(true);
486 }
487
488 // First create the image provider
489 ImageHelper imageProvider = createImageProvider(config,
490 options.packagedModulesPath,
491 options.ignoreSigning,
492 options.bindServices,
493 options.endian,
494 options.verbose,
495 options,
496 log);
497
498 // Then create the Plugin Stack
499 ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
500 taskHelper.getPluginsConfig(options.output, options.launchers,
501 imageProvider.targetPlatform));
502
503 //Ask the stack to proceed
504 stack.operate(imageProvider);
505 }
506
507 /**
508 * @return the system module path or null
509 */
510 public static Path getDefaultModulePath() {
511 Path jmods = Paths.get(System.getProperty("java.home"), "jmods");
512 return Files.isDirectory(jmods)? jmods : null;
513 }
514
515 /*
516 * Returns a module finder of the given module finder that limits the
517 * observable modules to those in the transitive closure of the modules
518 * specified in {@code limitMods} plus other modules specified in the
519 * {@code roots} set.
520 */
521 public static ModuleFinder limitFinder(ModuleFinder finder,
522 Set<String> limitMods,
523 Set<String> roots) {
524 // if limitMods is specified then limit the universe
525 if (limitMods != null && !limitMods.isEmpty()) {
526 Objects.requireNonNull(roots);
527 // resolve all root modules
528 Configuration cf = Configuration.empty()
529 .resolve(finder,
530 ModuleFinder.of(),
531 limitMods);
532
533 // module name -> reference
534 Map<String, ModuleReference> map = new HashMap<>();
535 cf.modules().forEach(m -> {
536 ModuleReference mref = m.reference();
537 map.put(mref.descriptor().name(), mref);
538 });
539
540 // add the other modules
541 roots.stream()
542 .map(finder::find)
543 .flatMap(Optional::stream)
544 .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref));
545
546 // set of modules that are observable
547 Set<ModuleReference> mrefs = new HashSet<>(map.values());
548
549 return new ModuleFinder() {
550 @Override
551 public Optional<ModuleReference> find(String name) {
552 return Optional.ofNullable(map.get(name));
553 }
554
555 @Override
556 public Set<ModuleReference> findAll() {
557 return mrefs;
558 }
559 };
560 }
561 return finder;
562 }
563
564 /*
565 * Checks the version of the module descriptor of java.base for compatibility
566 * with the current runtime version.
567 *
568 * @throws IllegalArgumentException the descriptor of java.base has no
569 * version or the java.base version is not the same as the current runtime's
570 * version.
571 */
572 private static void checkJavaBaseVersion(ModuleFinder finder) {
573 assert finder.find("java.base").isPresent();
574
575 // use the version of java.base module, if present, as
576 // the release version for multi-release JAR files
577 ModuleDescriptor.Version v = finder.find("java.base").get()
578 .descriptor().version().orElseThrow(() ->
579 new IllegalArgumentException("No version in java.base descriptor")
580 );
581
582 Runtime.Version version = Runtime.Version.parse(v.toString());
583 if (Runtime.version().feature() != version.feature() ||
584 Runtime.version().interim() != version.interim()) {
585 // jlink version and java.base version do not match.
586 // We do not (yet) support this mode.
587 throw new IllegalArgumentException(taskHelper.getMessage("err.jlink.version.mismatch",
588 Runtime.version().feature(), Runtime.version().interim(),
589 version.feature(), version.interim()));
590 }
591 }
592
593 private static void deleteDirectory(Path dir) throws IOException {
594 Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
595 @Override
596 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
597 throws IOException {
598 Files.delete(file);
599 return FileVisitResult.CONTINUE;
600 }
601 @Override
602 public FileVisitResult postVisitDirectory(Path dir, IOException e)
603 throws IOException {
604 if (e == null) {
605 Files.delete(dir);
606 return FileVisitResult.CONTINUE;
607 } else {
608 // directory iteration failed.
609 throw e;
610 }
611 }
612 });
613 }
614
615 private static Path toPathLocation(ResolvedModule m) {
616 Optional<URI> ouri = m.reference().location();
617 if (ouri.isEmpty()) {
618 throw new InternalError(m + " does not have a location");
619 }
620 URI uri = ouri.get();
621 return Paths.get(uri);
622 }
623
624
625 private static ImageHelper createImageProvider(JlinkConfiguration config,
626 Path retainModulesPath,
627 boolean ignoreSigning,
628 boolean bindService,
629 ByteOrder endian,
630 boolean verbose,
631 OptionsValues opts,
632 PrintWriter log)
633 throws IOException
634 {
635 Configuration cf = bindService ? config.resolveAndBind()
636 : config.resolve();
637
638 cf.modules().stream()
639 .map(ResolvedModule::reference)
640 .filter(mref -> mref.descriptor().isAutomatic())
641 .findAny()
642 .ifPresent(mref -> {
643 String loc = mref.location().map(URI::toString).orElse("<unknown>");
644 throw new IllegalArgumentException(
645 taskHelper.getMessage("err.automatic.module", mref.descriptor().name(), loc));
646 });
647
648 // Perform some sanity checks for linking from the run-time image
649 if (config.linkFromRuntimeImage()) {
650 // Do not permit linking from run-time image and also including jdk.jlink module
651 if (cf.findModule(JlinkTask.class.getModule().getName()).isPresent()) {
652 String msg = taskHelper.getMessage("err.runtime.link.jdk.jlink.prohibited");
653 throw new IllegalArgumentException(msg);
654 }
655 // Do not permit linking from run-time image when the current image
656 // is being patched.
657 if (ModuleBootstrap.patcher().hasPatches()) {
658 String msg = taskHelper.getMessage("err.runtime.link.patched.module");
659 throw new IllegalArgumentException(msg);
660 }
661
662 // Print info message indicating linking from the run-time image
663 if (verbose && log != null) {
664 log.println(taskHelper.getMessage("runtime.link.info"));
665 }
666 }
667
668 if (verbose && log != null) {
669 // print modules to be linked in
670 cf.modules().stream()
671 .sorted(Comparator.comparing(ResolvedModule::name))
672 .forEach(rm -> log.format("%s %s%s%n",
673 rm.name(),
674 rm.reference().location().get(),
675 // We have a link from run-time image when scheme is 'jrt'
676 "jrt".equals(rm.reference().location().get().getScheme())
677 ? " " + taskHelper.getMessage("runtime.link.jprt.path.extra")
678 : ""));
679
680 // print provider info
681 Set<ModuleReference> references = cf.modules().stream()
682 .map(ResolvedModule::reference).collect(Collectors.toSet());
683
684 String msg = String.format("%n%s:", taskHelper.getMessage("providers.header"));
685 printProviders(log, msg, references);
686 }
687
688 // emit a warning for any incubating modules in the configuration
689 if (log != null) {
690 String im = cf.modules()
691 .stream()
692 .map(ResolvedModule::reference)
693 .filter(ModuleResolution::hasIncubatingWarning)
694 .map(ModuleReference::descriptor)
695 .map(ModuleDescriptor::name)
696 .collect(Collectors.joining(", "));
697
698 if (!"".equals(im)) {
699 log.println("WARNING: Using incubator modules: " + im);
700 }
701 }
702
703 Map<String, Path> mods = cf.modules().stream()
704 .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation));
705 // determine the target platform of the image being created
706 Platform targetPlatform = targetPlatform(cf, mods, config.linkFromRuntimeImage());
707 // if the user specified any --endian, then it must match the target platform's native
708 // endianness
709 if (endian != null && endian != targetPlatform.arch().byteOrder()) {
710 throw new IOException(
711 taskHelper.getMessage("err.target.endianness.mismatch", endian, targetPlatform));
712 }
713 if (verbose && log != null) {
714 Platform runtime = Platform.runtime();
715 if (runtime.os() != targetPlatform.os() || runtime.arch() != targetPlatform.arch()) {
716 log.format("Cross-platform image generation, using %s for target platform %s%n",
717 targetPlatform.arch().byteOrder(), targetPlatform);
718 }
719 }
720
721 // use the version of java.base module, if present, as
722 // the release version for multi-release JAR files
723 var version = cf.findModule("java.base")
724 .map(ResolvedModule::reference)
725 .map(ModuleReference::descriptor)
726 .flatMap(ModuleDescriptor::version)
727 .map(ModuleDescriptor.Version::toString)
728 .map(Runtime.Version::parse)
729 .orElse(Runtime.version());
730
731 Set<Archive> archives = mods.entrySet().stream()
732 .map(e -> newArchive(e.getKey(),
733 e.getValue(),
734 version,
735 ignoreSigning,
736 config,
737 log))
738 .collect(Collectors.toSet());
739
740 return new ImageHelper(archives,
741 targetPlatform,
742 retainModulesPath,
743 config.isGenerateRuntimeImage());
744 }
745
746 private static Archive newArchive(String module,
747 Path path,
748 Runtime.Version version,
749 boolean ignoreSigning,
750 JlinkConfiguration config,
751 PrintWriter log) {
752 if (path.toString().endsWith(".jmod")) {
753 return new JmodArchive(module, path);
754 } else if (path.toString().endsWith(".jar")) {
755 ModularJarArchive modularJarArchive = new ModularJarArchive(module, path, version);
756 try (Stream<Archive.Entry> entries = modularJarArchive.entries()) {
757 boolean hasSignatures = entries.anyMatch((entry) -> {
758 String name = entry.name().toUpperCase(Locale.ROOT);
759
760 return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && (
761 name.endsWith(".SF") ||
762 name.endsWith(".DSA") ||
763 name.endsWith(".RSA") ||
764 name.endsWith(".EC") ||
765 name.startsWith("META-INF/SIG-")
766 );
767 });
768
769 if (hasSignatures) {
770 if (ignoreSigning) {
771 System.err.println(taskHelper.getMessage("warn.signing", path));
772 } else {
773 throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path));
774 }
775 }
776 }
777 return modularJarArchive;
778 } else if (Files.isDirectory(path) && !"jrt".equals(path.toUri().getScheme())) {
779 // The jrt URI path scheme conditional is there since we'd otherwise
780 // enter this branch for linking from the run-time image where the
781 // path is a jrt path. Note that the specific module would be a
782 // directory. I.e. Files.isDirectory() would be true.
783 Path modInfoPath = path.resolve("module-info.class");
784 if (Files.isRegularFile(modInfoPath)) {
785 return new DirArchive(path, findModuleName(modInfoPath));
786 } else {
787 throw new IllegalArgumentException(
788 taskHelper.getMessage("err.not.a.module.directory", path));
789 }
790 } else if (config.linkFromRuntimeImage()) {
791 return LinkableRuntimeImage.newArchive(module, path, config.ignoreModifiedRuntime(), taskHelper);
792 } else {
793 throw new IllegalArgumentException(
794 taskHelper.getMessage("err.not.modular.format", module, path));
795 }
796 }
797
798 private static String findModuleName(Path modInfoPath) {
799 try (BufferedInputStream bis = new BufferedInputStream(
800 Files.newInputStream(modInfoPath))) {
801 return ModuleDescriptor.read(bis).name();
802 } catch (IOException exp) {
803 throw new IllegalArgumentException(taskHelper.getMessage(
804 "err.cannot.read.module.info", modInfoPath), exp);
805 }
806 }
807
808 private static Platform targetPlatform(Configuration cf,
809 Map<String, Path> modsPaths,
810 boolean runtimeImageLink) throws IOException {
811 Path javaBasePath = modsPaths.get("java.base");
812 assert javaBasePath != null : "java.base module path is missing";
813 if (runtimeImageLink || isJavaBaseFromDefaultModulePath(javaBasePath)) {
814 // this implies that the java.base module used for the target image
815 // will correspond to the current platform. So this isn't an attempt to
816 // build a cross-platform image. We use the current platform's endianness
817 // in this case
818 return Platform.runtime();
819 } else {
820 // this is an attempt to build a cross-platform image. We now attempt to
821 // find the target platform's arch and thus its endianness from the java.base
822 // module's ModuleTarget attribute
823 String targetPlatformVal = readJavaBaseTargetPlatform(cf);
824 try {
825 return Platform.parsePlatform(targetPlatformVal);
826 } catch (IllegalArgumentException iae) {
827 throw new IOException(
828 taskHelper.getMessage("err.unknown.target.platform", targetPlatformVal));
829 }
830 }
831 }
832
833 // returns true if the default module-path is the parent of the passed javaBasePath
834 private static boolean isJavaBaseFromDefaultModulePath(Path javaBasePath) throws IOException {
835 Path defaultModulePath = getDefaultModulePath();
836 if (defaultModulePath == null) {
837 return false;
838 }
839 // resolve, against the default module-path dir, the java.base module file used
840 // for image creation
841 Path javaBaseInDefaultPath = defaultModulePath.resolve(javaBasePath.getFileName());
842 if (Files.notExists(javaBaseInDefaultPath)) {
843 // the java.base module used for image creation doesn't exist in the default
844 // module path
845 return false;
846 }
847 return Files.isSameFile(javaBasePath, javaBaseInDefaultPath);
848 }
849
850 // returns the targetPlatform value from the ModuleTarget attribute of the java.base module.
851 // throws IOException if the targetPlatform cannot be determined.
852 private static String readJavaBaseTargetPlatform(Configuration cf) throws IOException {
853 Optional<ResolvedModule> javaBase = cf.findModule("java.base");
854 assert javaBase.isPresent() : "java.base module is missing";
855 ModuleReference ref = javaBase.get().reference();
856 if (ref instanceof ModuleReferenceImpl modRefImpl
857 && modRefImpl.moduleTarget() != null) {
858 return modRefImpl.moduleTarget().targetPlatform();
859 }
860 // could not determine target platform
861 throw new IOException(
862 taskHelper.getMessage("err.cannot.determine.target.platform",
863 ref.location().map(URI::toString)
864 .orElse("java.base module")));
865 }
866
867 /*
868 * Returns a map of each service type to the modules that use it
869 * It will include services that are provided by a module but may not used
870 * by any of the observable modules.
871 */
872 private static Map<String, Set<String>> uses(Set<ModuleReference> modules) {
873 // collects the services used by the modules and print uses
874 Map<String, Set<String>> services = new HashMap<>();
875 modules.stream()
876 .map(ModuleReference::descriptor)
877 .forEach(md -> {
878 // include services that may not be used by any observable modules
879 md.provides().forEach(p ->
880 services.computeIfAbsent(p.service(), _k -> new HashSet<>()));
881 md.uses().forEach(s -> services.computeIfAbsent(s, _k -> new HashSet<>())
882 .add(md.name()));
883 });
884 return services;
885 }
886
887 private static void printProviders(PrintWriter log,
888 String header,
889 Set<ModuleReference> modules) {
890 printProviders(log, header, modules, uses(modules));
891 }
892
893 /*
894 * Prints the providers that are used by the specified services.
895 *
896 * The specified services maps a service type name to the modules
897 * using the service type which may be empty if no observable module uses
898 * that service.
899 */
900 private static void printProviders(PrintWriter log,
901 String header,
902 Set<ModuleReference> modules,
903 Map<String, Set<String>> serviceToUses) {
904 if (modules.isEmpty()) {
905 return;
906 }
907
908 // Build a map of a service type to the provider modules
909 Map<String, Set<ModuleDescriptor>> providers = new HashMap<>();
910 modules.stream()
911 .map(ModuleReference::descriptor)
912 .forEach(md -> {
913 md.provides().stream()
914 .filter(p -> serviceToUses.containsKey(p.service()))
915 .forEach(p -> providers.computeIfAbsent(p.service(), _k -> new HashSet<>())
916 .add(md));
917 });
918
919 if (!providers.isEmpty()) {
920 log.println(header);
921 }
922
923 // print the providers of the service types used by the specified modules
924 // sorted by the service type name and then provider's module name
925 providers.entrySet().stream()
926 .sorted(Map.Entry.comparingByKey())
927 .forEach(e -> {
928 String service = e.getKey();
929 e.getValue().stream()
930 .sorted(Comparator.comparing(ModuleDescriptor::name))
931 .forEach(md ->
932 md.provides().stream()
933 .filter(p -> p.service().equals(service))
934 .forEach(p -> {
935 String usedBy;
936 if (serviceToUses.get(p.service()).isEmpty()) {
937 usedBy = "not used by any observable module";
938 } else {
939 usedBy = serviceToUses.get(p.service()).stream()
940 .sorted()
941 .collect(Collectors.joining(",", "used by ", ""));
942 }
943 log.format(" %s provides %s %s%n",
944 md.name(), p.service(), usedBy);
945 })
946 );
947 });
948 }
949
950 private void suggestProviders(JlinkConfiguration config, List<String> args)
951 throws BadArgs
952 {
953 if (args.size() > 1) {
954 List<String> arguments = args.get(0).startsWith("-")
955 ? args
956 : args.subList(1, args.size());
957 throw taskHelper.newBadArgs("err.invalid.arg.for.option",
958 "--suggest-providers",
959 arguments.stream().collect(Collectors.joining(" ")));
960 }
961
962 if (options.bindServices) {
963 log.println(taskHelper.getMessage("no.suggested.providers"));
964 return;
965 }
966
967 ModuleFinder finder = config.finder();
968 if (args.isEmpty()) {
969 // print providers used by the observable modules without service binding
970 Set<ModuleReference> mrefs = finder.findAll();
971 // print uses of the modules that would be linked into the image
972 mrefs.stream()
973 .sorted(Comparator.comparing(mref -> mref.descriptor().name()))
974 .forEach(mref -> {
975 ModuleDescriptor md = mref.descriptor();
976 log.format("%s %s%n", md.name(),
977 mref.location().get());
978 md.uses().stream().sorted()
979 .forEach(s -> log.format(" uses %s%n", s));
980 });
981
982 String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
983 printProviders(log, msg, mrefs, uses(mrefs));
984
985 } else {
986 // comma-separated service types, if specified
987 Set<String> names = Stream.of(args.get(0).split(","))
988 .collect(Collectors.toSet());
989 // find the modules that provide the specified service
990 Set<ModuleReference> mrefs = finder.findAll().stream()
991 .filter(mref -> mref.descriptor().provides().stream()
992 .map(ModuleDescriptor.Provides::service)
993 .anyMatch(names::contains))
994 .collect(Collectors.toSet());
995
996 // find the modules that uses the specified services
997 Map<String, Set<String>> uses = new HashMap<>();
998 names.forEach(s -> uses.computeIfAbsent(s, _k -> new HashSet<>()));
999 finder.findAll().stream()
1000 .map(ModuleReference::descriptor)
1001 .forEach(md -> md.uses().stream()
1002 .filter(names::contains)
1003 .forEach(s -> uses.get(s).add(md.name())));
1004
1005 // check if any name given on the command line are not provided by any module
1006 mrefs.stream()
1007 .flatMap(mref -> mref.descriptor().provides().stream()
1008 .map(ModuleDescriptor.Provides::service))
1009 .forEach(names::remove);
1010 if (!names.isEmpty()) {
1011 log.println(taskHelper.getMessage("warn.provider.notfound",
1012 names.stream().sorted().collect(Collectors.joining(","))));
1013 }
1014
1015 String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
1016 printProviders(log, msg, mrefs, uses);
1017 }
1018 }
1019
1020 private String getSaveOpts() {
1021 StringBuilder sb = new StringBuilder();
1022 sb.append('#').append(new Date()).append("\n");
1023 for (String c : optionsHelper.getInputCommand()) {
1024 sb.append(c).append(" ");
1025 }
1026
1027 return sb.toString();
1028 }
1029
1030 private static record ImageHelper(Set<Archive> archives,
1031 Platform targetPlatform,
1032 Path packagedModulesPath,
1033 boolean generateRuntimeImage) implements ImageProvider {
1034 @Override
1035 public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
1036 ExecutableImage image = ImageFileCreator.create(archives,
1037 targetPlatform.arch().byteOrder(), stack, generateRuntimeImage);
1038 if (packagedModulesPath != null) {
1039 // copy the packaged modules to the given path
1040 Files.createDirectories(packagedModulesPath);
1041 for (Archive a : archives) {
1042 Path file = a.getPath();
1043 Path dest = packagedModulesPath.resolve(file.getFileName());
1044 Files.copy(file, dest);
1045 }
1046 }
1047 return image;
1048 }
1049 }
1050 }