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