1 /*
2 * Copyright (c) 2015, 2025, 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 try (ImageHelper 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
370 // the token for "all modules on the module path"
371 private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
372 private JlinkConfiguration initJlinkConfig() throws BadArgs {
373 // Empty module path not allowed with ALL-MODULE-PATH in --add-modules
374 if (options.addMods.contains(ALL_MODULE_PATH) && options.modulePath.isEmpty()) {
375 throw taskHelper.newBadArgs("err.no.module.path");
376 }
377 ModuleFinder appModuleFinder = newModuleFinder(options.modulePath);
378 ModuleFinder finder = appModuleFinder;
379
380 boolean isLinkFromRuntime = false;
381 if (!appModuleFinder.find("java.base").isPresent()) {
382 // If the application module finder doesn't contain the
383 // java.base module we have one of two cases:
384 // 1. A custom module is being linked into a runtime, but the JDK
385 // modules have not been provided on the module path.
386 // 2. We have a run-time image based link.
387 //
388 // Distinguish case 2 by adding the default 'jmods' folder and try
389 // the look-up again. For case 1 this will now find java.base, but
390 // not for case 2, since the jmods folder is not there or doesn't
391 // include the java.base module.
392 Path defModPath = getDefaultModulePath();
393 if (defModPath != null) {
394 List<Path> combinedPaths = new ArrayList<>(options.modulePath);
395 combinedPaths.add(defModPath);
396 finder = newModuleFinder(combinedPaths);
397 }
398 // We've just added the default module path ('jmods'). If we still
399 // don't find java.base, we must resolve JDK modules from the
400 // current run-time image.
401 if (!finder.find("java.base").isPresent()) {
402 // If we don't have a linkable run-time image this is an error
403 if (!LinkableRuntimeImage.isLinkableRuntime()) {
404 throw taskHelper.newBadArgs("err.runtime.link.not.linkable.runtime");
405 }
406 isLinkFromRuntime = true;
407 // JDK modules come from the system module path
408 finder = ModuleFinder.compose(ModuleFinder.ofSystem(), appModuleFinder);
409 }
410 }
411
412 // Sanity check version if we use JMODs
413 if (!isLinkFromRuntime) {
414 checkJavaBaseVersion(finder);
415 }
416
417 // Determine the roots set
418 Set<String> roots = new HashSet<>();
419 for (String mod : options.addMods) {
420 if (mod.equals(ALL_MODULE_PATH)) {
421 // Using --limit-modules with ALL-MODULE-PATH is an error
422 if (!options.limitMods.isEmpty()) {
423 throw taskHelper.newBadArgs("err.limit.modules");
424 }
425 // all observable modules in the app module path are roots
426 Set<String> initialRoots = appModuleFinder.findAll()
427 .stream()
428 .map(ModuleReference::descriptor)
429 .map(ModuleDescriptor::name)
430 .collect(Collectors.toSet());
431
432 // Error if no module is found on the app module path
433 if (initialRoots.isEmpty()) {
434 String modPath = options.modulePath.stream()
435 .map(a -> a.toString())
436 .collect(Collectors.joining(", "));
437 throw taskHelper.newBadArgs("err.empty.module.path", modPath);
438 }
439
440 // Use a module finder with limited observability, as determined
441 // by initialRoots, to find the observable modules from the
442 // application module path (--module-path option) only. We must
443 // not include JDK modules from the default module path or the
444 // run-time image.
445 ModuleFinder mf = limitFinder(finder, initialRoots, Set.of());
446 mf.findAll()
447 .stream()
448 .map(ModuleReference::descriptor)
449 .map(ModuleDescriptor::name)
450 .forEach(mn -> roots.add(mn));
451 } else {
452 roots.add(mod);
453 }
454 }
455 finder = limitFinder(finder, options.limitMods, roots);
456
457 // --keep-packaged-modules doesn't make sense as we are not linking
458 // from packaged modules to begin with.
459 if (isLinkFromRuntime && options.packagedModulesPath != null) {
460 throw taskHelper.newBadArgs("err.runtime.link.packaged.mods");
461 }
462
463 return new JlinkConfiguration(options.output,
464 roots,
465 finder,
466 isLinkFromRuntime,
467 options.ignoreModifiedRuntime,
468 options.generateLinkableRuntime);
469 }
470
471 /*
472 * Creates a ModuleFinder for the given module paths.
473 */
474 public static ModuleFinder newModuleFinder(List<Path> paths) {
475 Runtime.Version version = Runtime.version();
476 Path[] entries = paths.toArray(new Path[0]);
477 return ModulePath.of(version, true, entries);
478 }
479
480 private void createImage(JlinkConfiguration config) throws Exception {
481 if (options.output == null) {
482 throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true);
483 }
484 if (options.addMods.isEmpty()) {
485 throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules")
486 .showUsage(true);
487 }
488
489 // First create the image provider
490 try (ImageHelper imageProvider = createImageProvider(config,
491 options.packagedModulesPath,
492 options.ignoreSigning,
493 options.bindServices,
494 options.endian,
495 options.verbose,
496 options,
497 log)) {
498 // Then create the Plugin Stack
499 ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
500 taskHelper.getPluginsConfig(
501 options.output,
502 options.launchers,
503 imageProvider.targetPlatform));
504
505 //Ask the stack to proceed
506 stack.operate(imageProvider);
507 }
508 }
509
510 /**
511 * @return the system module path or null
512 */
513 public static Path getDefaultModulePath() {
514 Path jmods = Paths.get(System.getProperty("java.home"), "jmods");
515 return Files.isDirectory(jmods)? jmods : null;
516 }
517
518 /*
519 * Returns a module finder of the given module finder that limits the
520 * observable modules to those in the transitive closure of the modules
521 * specified in {@code limitMods} plus other modules specified in the
522 * {@code roots} set.
523 */
524 public static ModuleFinder limitFinder(ModuleFinder finder,
525 Set<String> limitMods,
526 Set<String> roots) {
527 // if limitMods is specified then limit the universe
528 if (limitMods != null && !limitMods.isEmpty()) {
529 Objects.requireNonNull(roots);
530 // resolve all root modules
531 Configuration cf = Configuration.empty()
532 .resolve(finder,
533 ModuleFinder.of(),
534 limitMods);
535
536 // module name -> reference
537 Map<String, ModuleReference> map = new HashMap<>();
538 cf.modules().forEach(m -> {
539 ModuleReference mref = m.reference();
540 map.put(mref.descriptor().name(), mref);
541 });
542
543 // add the other modules
544 roots.stream()
545 .map(finder::find)
546 .flatMap(Optional::stream)
547 .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref));
548
549 // set of modules that are observable
550 Set<ModuleReference> mrefs = new HashSet<>(map.values());
551
552 return new ModuleFinder() {
553 @Override
554 public Optional<ModuleReference> find(String name) {
555 return Optional.ofNullable(map.get(name));
556 }
557
558 @Override
559 public Set<ModuleReference> findAll() {
560 return mrefs;
561 }
562 };
563 }
564 return finder;
565 }
566
567 /*
568 * Checks the version of the module descriptor of java.base for compatibility
569 * with the current runtime version.
570 *
571 * @throws IllegalArgumentException the descriptor of java.base has no
572 * version or the java.base version is not the same as the current runtime's
573 * version.
574 */
575 private static void checkJavaBaseVersion(ModuleFinder finder) {
576 assert finder.find("java.base").isPresent();
577
578 // use the version of java.base module, if present, as
579 // the release version for multi-release JAR files
580 ModuleDescriptor.Version v = finder.find("java.base").get()
581 .descriptor().version().orElseThrow(() ->
582 new IllegalArgumentException("No version in java.base descriptor")
583 );
584
585 Runtime.Version version = Runtime.Version.parse(v.toString());
586 if (Runtime.version().feature() != version.feature() ||
587 Runtime.version().interim() != version.interim()) {
588 // jlink version and java.base version do not match.
589 // We do not (yet) support this mode.
590 throw new IllegalArgumentException(taskHelper.getMessage("err.jlink.version.mismatch",
591 Runtime.version().feature(), Runtime.version().interim(),
592 version.feature(), version.interim()));
593 }
594 }
595
596 private static void deleteDirectory(Path dir) throws IOException {
597 Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
598 @Override
599 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
600 throws IOException {
601 Files.delete(file);
602 return FileVisitResult.CONTINUE;
603 }
604 @Override
605 public FileVisitResult postVisitDirectory(Path dir, IOException e)
606 throws IOException {
607 if (e == null) {
608 Files.delete(dir);
609 return FileVisitResult.CONTINUE;
610 } else {
611 // directory iteration failed.
612 throw e;
613 }
614 }
615 });
616 }
617
618 private static Path toPathLocation(ResolvedModule m) {
619 Optional<URI> ouri = m.reference().location();
620 if (ouri.isEmpty()) {
621 throw new InternalError(m + " does not have a location");
622 }
623 URI uri = ouri.get();
624 return Paths.get(uri);
625 }
626
627
628 private static ImageHelper createImageProvider(JlinkConfiguration config,
629 Path retainModulesPath,
630 boolean ignoreSigning,
631 boolean bindService,
632 ByteOrder endian,
633 boolean verbose,
634 OptionsValues opts,
635 PrintWriter log)
636 throws IOException
637 {
638 Configuration cf = bindService ? config.resolveAndBind()
639 : config.resolve();
640
641 cf.modules().stream()
642 .map(ResolvedModule::reference)
643 .filter(mref -> mref.descriptor().isAutomatic())
644 .findAny()
645 .ifPresent(mref -> {
646 String loc = mref.location().map(URI::toString).orElse("<unknown>");
647 throw new IllegalArgumentException(
648 taskHelper.getMessage("err.automatic.module", mref.descriptor().name(), loc));
649 });
650
651 // Perform some sanity checks for linking from the run-time image
652 if (config.linkFromRuntimeImage()) {
653 // Do not permit linking from run-time image and also including jdk.jlink module
654 if (cf.findModule(JlinkTask.class.getModule().getName()).isPresent()) {
655 String msg = taskHelper.getMessage("err.runtime.link.jdk.jlink.prohibited");
656 throw new IllegalArgumentException(msg);
657 }
658 // Do not permit linking from run-time image when the current image
659 // is being patched.
660 if (ModuleBootstrap.patcher().hasPatches()) {
661 String msg = taskHelper.getMessage("err.runtime.link.patched.module");
662 throw new IllegalArgumentException(msg);
663 }
664
665 // Print info message indicating linking from the run-time image
666 if (verbose && log != null) {
667 log.println(taskHelper.getMessage("runtime.link.info"));
668 }
669 }
670
671 if (verbose && log != null) {
672 // print modules to be linked in
673 cf.modules().stream()
674 .sorted(Comparator.comparing(ResolvedModule::name))
675 .forEach(rm -> log.format("%s %s%s%n",
676 rm.name(),
677 rm.reference().location().get(),
678 // We have a link from run-time image when scheme is 'jrt'
679 "jrt".equals(rm.reference().location().get().getScheme())
680 ? " " + taskHelper.getMessage("runtime.link.jprt.path.extra")
681 : ""));
682
683 // print provider info
684 Set<ModuleReference> references = cf.modules().stream()
685 .map(ResolvedModule::reference).collect(Collectors.toSet());
686
687 String msg = String.format("%n%s:", taskHelper.getMessage("providers.header"));
688 printProviders(log, msg, references);
689 }
690
691 // emit a warning for any incubating modules in the configuration
692 if (log != null) {
693 String im = cf.modules()
694 .stream()
695 .map(ResolvedModule::reference)
696 .filter(ModuleResolution::hasIncubatingWarning)
697 .map(ModuleReference::descriptor)
698 .map(ModuleDescriptor::name)
699 .collect(Collectors.joining(", "));
700
701 if (!"".equals(im)) {
702 log.println("WARNING: Using incubator modules: " + im);
703 }
704 }
705
706 Map<String, Path> mods = cf.modules().stream()
707 .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation));
708 // determine the target platform of the image being created
709 Platform targetPlatform = targetPlatform(cf, mods, config.linkFromRuntimeImage());
710 // if the user specified any --endian, then it must match the target platform's native
711 // endianness
712 if (endian != null && endian != targetPlatform.arch().byteOrder()) {
713 throw new IOException(
714 taskHelper.getMessage("err.target.endianness.mismatch", endian, targetPlatform));
715 }
716 if (verbose && log != null) {
717 Platform runtime = Platform.runtime();
718 if (runtime.os() != targetPlatform.os() || runtime.arch() != targetPlatform.arch()) {
719 log.format("Cross-platform image generation, using %s for target platform %s%n",
720 targetPlatform.arch().byteOrder(), targetPlatform);
721 }
722 }
723
724 // use the version of java.base module, if present, as
725 // the release version for multi-release JAR files
726 var version = cf.findModule("java.base")
727 .map(ResolvedModule::reference)
728 .map(ModuleReference::descriptor)
729 .flatMap(ModuleDescriptor::version)
730 .map(ModuleDescriptor.Version::toString)
731 .map(Runtime.Version::parse)
732 .orElse(Runtime.version());
733
734 Set<Archive> archives = mods.entrySet().stream()
735 .map(e -> newArchive(e.getKey(),
736 e.getValue(),
737 version,
738 ignoreSigning,
739 config,
740 log))
741 .collect(Collectors.toSet());
742
743 return new ImageHelper(archives,
744 targetPlatform,
745 retainModulesPath,
746 config.isGenerateRuntimeImage());
747 }
748
749 private static Archive newArchive(String module,
750 Path path,
751 Runtime.Version version,
752 boolean ignoreSigning,
753 JlinkConfiguration config,
754 PrintWriter log) {
755 if (path.toString().endsWith(".jmod")) {
756 return new JmodArchive(module, path);
757 } else if (path.toString().endsWith(".jar")) {
758 ModularJarArchive modularJarArchive = new ModularJarArchive(module, path, version);
759 try (Stream<Archive.Entry> entries = modularJarArchive.entries()) {
760 boolean hasSignatures = entries.anyMatch((entry) -> {
761 String name = entry.name().toUpperCase(Locale.ROOT);
762
763 return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && (
764 name.endsWith(".SF") ||
765 name.endsWith(".DSA") ||
766 name.endsWith(".RSA") ||
767 name.endsWith(".EC") ||
768 name.startsWith("META-INF/SIG-")
769 );
770 });
771
772 if (hasSignatures) {
773 if (ignoreSigning) {
774 System.err.println(taskHelper.getMessage("warn.signing", path));
775 } else {
776 throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path));
777 }
778 }
779 }
780 return modularJarArchive;
781 } else if (Files.isDirectory(path) && !"jrt".equals(path.toUri().getScheme())) {
782 // The jrt URI path scheme conditional is there since we'd otherwise
783 // enter this branch for linking from the run-time image where the
784 // path is a jrt path. Note that the specific module would be a
785 // directory. I.e. Files.isDirectory() would be true.
786 Path modInfoPath = path.resolve("module-info.class");
787 if (Files.isRegularFile(modInfoPath)) {
788 return new DirArchive(path, findModuleName(modInfoPath));
789 } else {
790 throw new IllegalArgumentException(
791 taskHelper.getMessage("err.not.a.module.directory", path));
792 }
793 } else if (config.linkFromRuntimeImage()) {
794 return LinkableRuntimeImage.newArchive(module, path, config.ignoreModifiedRuntime(), taskHelper);
795 } else {
796 throw new IllegalArgumentException(
797 taskHelper.getMessage("err.not.modular.format", module, path));
798 }
799 }
800
801 private static String findModuleName(Path modInfoPath) {
802 try (BufferedInputStream bis = new BufferedInputStream(
803 Files.newInputStream(modInfoPath))) {
804 return ModuleDescriptor.read(bis).name();
805 } catch (IOException exp) {
806 throw new IllegalArgumentException(taskHelper.getMessage(
807 "err.cannot.read.module.info", modInfoPath), exp);
808 }
809 }
810
811 private static Platform targetPlatform(Configuration cf,
812 Map<String, Path> modsPaths,
813 boolean runtimeImageLink) throws IOException {
814 Path javaBasePath = modsPaths.get("java.base");
815 assert javaBasePath != null : "java.base module path is missing";
816 if (runtimeImageLink || isJavaBaseFromDefaultModulePath(javaBasePath)) {
817 // this implies that the java.base module used for the target image
818 // will correspond to the current platform. So this isn't an attempt to
819 // build a cross-platform image. We use the current platform's endianness
820 // in this case
821 return Platform.runtime();
822 } else {
823 // this is an attempt to build a cross-platform image. We now attempt to
824 // find the target platform's arch and thus its endianness from the java.base
825 // module's ModuleTarget attribute
826 String targetPlatformVal = readJavaBaseTargetPlatform(cf);
827 try {
828 return Platform.parsePlatform(targetPlatformVal);
829 } catch (IllegalArgumentException iae) {
830 throw new IOException(
831 taskHelper.getMessage("err.unknown.target.platform", targetPlatformVal));
832 }
833 }
834 }
835
836 // returns true if the default module-path is the parent of the passed javaBasePath
837 private static boolean isJavaBaseFromDefaultModulePath(Path javaBasePath) throws IOException {
838 Path defaultModulePath = getDefaultModulePath();
839 if (defaultModulePath == null) {
840 return false;
841 }
842 // resolve, against the default module-path dir, the java.base module file used
843 // for image creation
844 Path javaBaseInDefaultPath = defaultModulePath.resolve(javaBasePath.getFileName());
845 if (Files.notExists(javaBaseInDefaultPath)) {
846 // the java.base module used for image creation doesn't exist in the default
847 // module path
848 return false;
849 }
850 return Files.isSameFile(javaBasePath, javaBaseInDefaultPath);
851 }
852
853 // returns the targetPlatform value from the ModuleTarget attribute of the java.base module.
854 // throws IOException if the targetPlatform cannot be determined.
855 private static String readJavaBaseTargetPlatform(Configuration cf) throws IOException {
856 Optional<ResolvedModule> javaBase = cf.findModule("java.base");
857 assert javaBase.isPresent() : "java.base module is missing";
858 ModuleReference ref = javaBase.get().reference();
859 if (ref instanceof ModuleReferenceImpl modRefImpl
860 && modRefImpl.moduleTarget() != null) {
861 return modRefImpl.moduleTarget().targetPlatform();
862 }
863 // could not determine target platform
864 throw new IOException(
865 taskHelper.getMessage("err.cannot.determine.target.platform",
866 ref.location().map(URI::toString)
867 .orElse("java.base module")));
868 }
869
870 /*
871 * Returns a map of each service type to the modules that use it
872 * It will include services that are provided by a module but may not used
873 * by any of the observable modules.
874 */
875 private static Map<String, Set<String>> uses(Set<ModuleReference> modules) {
876 // collects the services used by the modules and print uses
877 Map<String, Set<String>> services = new HashMap<>();
878 modules.stream()
879 .map(ModuleReference::descriptor)
880 .forEach(md -> {
881 // include services that may not be used by any observable modules
882 md.provides().forEach(p ->
883 services.computeIfAbsent(p.service(), _k -> new HashSet<>()));
884 md.uses().forEach(s -> services.computeIfAbsent(s, _k -> new HashSet<>())
885 .add(md.name()));
886 });
887 return services;
888 }
889
890 private static void printProviders(PrintWriter log,
891 String header,
892 Set<ModuleReference> modules) {
893 printProviders(log, header, modules, uses(modules));
894 }
895
896 /*
897 * Prints the providers that are used by the specified services.
898 *
899 * The specified services maps a service type name to the modules
900 * using the service type which may be empty if no observable module uses
901 * that service.
902 */
903 private static void printProviders(PrintWriter log,
904 String header,
905 Set<ModuleReference> modules,
906 Map<String, Set<String>> serviceToUses) {
907 if (modules.isEmpty()) {
908 return;
909 }
910
911 // Build a map of a service type to the provider modules
912 Map<String, Set<ModuleDescriptor>> providers = new HashMap<>();
913 modules.stream()
914 .map(ModuleReference::descriptor)
915 .forEach(md -> {
916 md.provides().stream()
917 .filter(p -> serviceToUses.containsKey(p.service()))
918 .forEach(p -> providers.computeIfAbsent(p.service(), _k -> new HashSet<>())
919 .add(md));
920 });
921
922 if (!providers.isEmpty()) {
923 log.println(header);
924 }
925
926 // print the providers of the service types used by the specified modules
927 // sorted by the service type name and then provider's module name
928 providers.entrySet().stream()
929 .sorted(Map.Entry.comparingByKey())
930 .forEach(e -> {
931 String service = e.getKey();
932 e.getValue().stream()
933 .sorted(Comparator.comparing(ModuleDescriptor::name))
934 .forEach(md ->
935 md.provides().stream()
936 .filter(p -> p.service().equals(service))
937 .forEach(p -> {
938 String usedBy;
939 if (serviceToUses.get(p.service()).isEmpty()) {
940 usedBy = "not used by any observable module";
941 } else {
942 usedBy = serviceToUses.get(p.service()).stream()
943 .sorted()
944 .collect(Collectors.joining(",", "used by ", ""));
945 }
946 log.format(" %s provides %s %s%n",
947 md.name(), p.service(), usedBy);
948 })
949 );
950 });
951 }
952
953 private void suggestProviders(JlinkConfiguration config, List<String> args)
954 throws BadArgs
955 {
956 if (args.size() > 1) {
957 List<String> arguments = args.get(0).startsWith("-")
958 ? args
959 : args.subList(1, args.size());
960 throw taskHelper.newBadArgs("err.invalid.arg.for.option",
961 "--suggest-providers",
962 arguments.stream().collect(Collectors.joining(" ")));
963 }
964
965 if (options.bindServices) {
966 log.println(taskHelper.getMessage("no.suggested.providers"));
967 return;
968 }
969
970 ModuleFinder finder = config.finder();
971 if (args.isEmpty()) {
972 // print providers used by the observable modules without service binding
973 Set<ModuleReference> mrefs = finder.findAll();
974 // print uses of the modules that would be linked into the image
975 mrefs.stream()
976 .sorted(Comparator.comparing(mref -> mref.descriptor().name()))
977 .forEach(mref -> {
978 ModuleDescriptor md = mref.descriptor();
979 log.format("%s %s%n", md.name(),
980 mref.location().get());
981 md.uses().stream().sorted()
982 .forEach(s -> log.format(" uses %s%n", s));
983 });
984
985 String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
986 printProviders(log, msg, mrefs, uses(mrefs));
987
988 } else {
989 // comma-separated service types, if specified
990 Set<String> names = Stream.of(args.get(0).split(","))
991 .collect(Collectors.toSet());
992 // find the modules that provide the specified service
993 Set<ModuleReference> mrefs = finder.findAll().stream()
994 .filter(mref -> mref.descriptor().provides().stream()
995 .map(ModuleDescriptor.Provides::service)
996 .anyMatch(names::contains))
997 .collect(Collectors.toSet());
998
999 // find the modules that uses the specified services
1000 Map<String, Set<String>> uses = new HashMap<>();
1001 names.forEach(s -> uses.computeIfAbsent(s, _k -> new HashSet<>()));
1002 finder.findAll().stream()
1003 .map(ModuleReference::descriptor)
1004 .forEach(md -> md.uses().stream()
1005 .filter(names::contains)
1006 .forEach(s -> uses.get(s).add(md.name())));
1007
1008 // check if any name given on the command line are not provided by any module
1009 mrefs.stream()
1010 .flatMap(mref -> mref.descriptor().provides().stream()
1011 .map(ModuleDescriptor.Provides::service))
1012 .forEach(names::remove);
1013 if (!names.isEmpty()) {
1014 log.println(taskHelper.getMessage("warn.provider.notfound",
1015 names.stream().sorted().collect(Collectors.joining(","))));
1016 }
1017
1018 String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
1019 printProviders(log, msg, mrefs, uses);
1020 }
1021 }
1022
1023 private String getSaveOpts() {
1024 StringBuilder sb = new StringBuilder();
1025 sb.append('#').append(new Date()).append("\n");
1026 for (String c : optionsHelper.getInputCommand()) {
1027 sb.append(c).append(" ");
1028 }
1029
1030 return sb.toString();
1031 }
1032
1033 private record ImageHelper(Set<Archive> archives,
1034 Platform targetPlatform,
1035 Path packagedModulesPath,
1036 boolean generateRuntimeImage)
1037 implements ImageProvider, AutoCloseable {
1038 @Override
1039 public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
1040 ExecutableImage image = ImageFileCreator.create(archives,
1041 targetPlatform.arch().byteOrder(), stack, generateRuntimeImage);
1042 if (packagedModulesPath != null) {
1043 // copy the packaged modules to the given path
1044 Files.createDirectories(packagedModulesPath);
1045 for (Archive a : archives) {
1046 Path file = a.getPath();
1047 Path dest = packagedModulesPath.resolve(file.getFileName());
1048 Files.copy(file, dest);
1049 }
1050 }
1051 return image;
1052 }
1053
1054 @Override
1055 public void close() throws IOException {
1056 List<IOException> thrown = null;
1057 for (Archive archive : archives) {
1058 try {
1059 archive.close();
1060 } catch (IOException ex) {
1061 if (thrown == null) {
1062 thrown = new ArrayList<>();
1063 }
1064 thrown.add(ex);
1065 }
1066 }
1067 if (thrown != null) {
1068 IOException ex = new IOException("Archives could not be closed", thrown.getFirst());
1069 thrown.subList(1, thrown.size()).forEach(ex::addSuppressed);
1070 throw ex;
1071 }
1072 }
1073 }
1074 }