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