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