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 }