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