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 }