1 /*
   2  * Copyright (c) 2015, 2017, 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 java.io.File;
  28 import java.io.IOException;
  29 import java.io.PrintWriter;
  30 import java.io.UncheckedIOException;
  31 import java.lang.module.Configuration;
  32 import java.lang.module.FindException;
  33 import java.lang.module.ModuleDescriptor;
  34 import java.lang.module.ModuleFinder;
  35 import java.lang.module.ModuleReference;
  36 import java.lang.module.ResolutionException;
  37 import java.lang.module.ResolvedModule;
  38 import java.net.URI;
  39 import java.nio.ByteOrder;
  40 import java.nio.file.Files;
  41 import java.nio.file.Path;
  42 import java.nio.file.Paths;
  43 import java.util.ArrayList;
  44 import java.util.Arrays;
  45 import java.util.Collection;
  46 import java.util.Collections;
  47 import java.util.Comparator;
  48 import java.util.Date;
  49 import java.util.HashMap;
  50 import java.util.HashSet;
  51 import java.util.List;
  52 import java.util.Locale;
  53 import java.util.Map;
  54 import java.util.Objects;
  55 import java.util.Optional;
  56 import java.util.Set;
  57 import java.util.stream.Collectors;
  58 import java.util.stream.Stream;
  59 
  60 import jdk.tools.jlink.internal.TaskHelper.BadArgs;
  61 import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE;
  62 import jdk.tools.jlink.internal.Jlink.JlinkConfiguration;
  63 import jdk.tools.jlink.internal.Jlink.PluginsConfiguration;
  64 import jdk.tools.jlink.internal.TaskHelper.Option;
  65 import jdk.tools.jlink.internal.TaskHelper.OptionsHelper;
  66 import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider;
  67 import jdk.tools.jlink.plugin.PluginException;
  68 import jdk.tools.jlink.builder.DefaultImageBuilder;
  69 import jdk.tools.jlink.plugin.Plugin;
  70 import jdk.internal.module.ModulePath;
  71 import jdk.internal.module.ModuleResolution;
  72 
  73 /**
  74  * Implementation for the jlink tool.
  75  *
  76  * ## Should use jdk.joptsimple some day.
  77  */
  78 public class JlinkTask {
  79     static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
  80 
  81     // jlink API ignores by default. Remove when signing is implemented.
  82     static final boolean IGNORE_SIGNING_DEFAULT = true;
  83 
  84     private static final TaskHelper taskHelper
  85             = new TaskHelper(JLINK_BUNDLE);
  86 
  87     private static final Option<?>[] recognizedOptions = {
  88         new Option<JlinkTask>(false, (task, opt, arg) -> {
  89             task.options.help = true;
  90         }, "--help", "-h"),
  91         new Option<JlinkTask>(true, (task, opt, arg) -> {
  92             // if used multiple times, the last one wins!
  93             // So, clear previous values, if any.
  94             task.options.modulePath.clear();
  95             String[] dirs = arg.split(File.pathSeparator);
  96             int i = 0;
  97             Arrays.stream(dirs)
  98                   .map(Paths::get)
  99                   .forEach(task.options.modulePath::add);
 100         }, "--module-path", "-p"),
 101         new Option<JlinkTask>(true, (task, opt, arg) -> {
 102             // if used multiple times, the last one wins!
 103             // So, clear previous values, if any.
 104             task.options.limitMods.clear();
 105             for (String mn : arg.split(",")) {
 106                 if (mn.isEmpty()) {
 107                     throw taskHelper.newBadArgs("err.mods.must.be.specified",
 108                             "--limit-modules");
 109                 }
 110                 task.options.limitMods.add(mn);
 111             }
 112         }, "--limit-modules"),
 113         new Option<JlinkTask>(true, (task, opt, arg) -> {
 114             for (String mn : arg.split(",")) {
 115                 if (mn.isEmpty()) {
 116                     throw taskHelper.newBadArgs("err.mods.must.be.specified",
 117                             "--add-modules");
 118                 }
 119                 task.options.addMods.add(mn);
 120             }
 121         }, "--add-modules"),
 122         new Option<JlinkTask>(true, (task, opt, arg) -> {
 123             Path path = Paths.get(arg);
 124             task.options.output = path;
 125         }, "--output"),
 126         new Option<JlinkTask>(false, (task, opt, arg) -> {
 127             task.options.bindServices = true;
 128         }, "--bind-services"),
 129         new Option<JlinkTask>(false, (task, opt, arg) -> {
 130             task.options.suggestProviders = true;
 131         }, "--suggest-providers", "", true),
 132         new Option<JlinkTask>(true, (task, opt, arg) -> {
 133             String[] values = arg.split("=");
 134             // check values
 135             if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) {
 136                 throw taskHelper.newBadArgs("err.launcher.value.format", arg);
 137             } else {
 138                 String commandName = values[0];
 139                 String moduleAndMain = values[1];
 140                 int idx = moduleAndMain.indexOf("/");
 141                 if (idx != -1) {
 142                     if (moduleAndMain.substring(0, idx).isEmpty()) {
 143                         throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg);
 144                     }
 145 
 146                     if (moduleAndMain.substring(idx + 1).isEmpty()) {
 147                         throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg);
 148                     }
 149                 }
 150                 task.options.launchers.put(commandName, moduleAndMain);
 151             }
 152         }, "--launcher"),
 153         new Option<JlinkTask>(true, (task, opt, arg) -> {
 154             if ("little".equals(arg)) {
 155                 task.options.endian = ByteOrder.LITTLE_ENDIAN;
 156             } else if ("big".equals(arg)) {
 157                 task.options.endian = ByteOrder.BIG_ENDIAN;
 158             } else {
 159                 throw taskHelper.newBadArgs("err.unknown.byte.order", arg);
 160             }
 161         }, "--endian"),
 162         new Option<JlinkTask>(false, (task, opt, arg) -> {
 163             task.options.verbose = true;
 164         }, "--verbose", "-v"),
 165         new Option<JlinkTask>(false, (task, opt, arg) -> {
 166             task.options.version = true;
 167         }, "--version"),
 168         new Option<JlinkTask>(true, (task, opt, arg) -> {
 169             Path path = Paths.get(arg);
 170             if (Files.exists(path)) {
 171                 throw taskHelper.newBadArgs("err.dir.exists", path);
 172             }
 173             task.options.packagedModulesPath = path;
 174         }, true, "--keep-packaged-modules"),
 175         new Option<JlinkTask>(true, (task, opt, arg) -> {
 176             task.options.saveoptsfile = arg;
 177         }, "--save-opts"),
 178         new Option<JlinkTask>(false, (task, opt, arg) -> {
 179             task.options.fullVersion = true;
 180         }, true, "--full-version"),
 181         new Option<JlinkTask>(false, (task, opt, arg) -> {
 182             task.options.ignoreSigning = true;
 183         }, "--ignore-signing-information"),};
 184 
 185     private static final String PROGNAME = "jlink";
 186     private final OptionsValues options = new OptionsValues();
 187 
 188     private static final OptionsHelper<JlinkTask> optionsHelper
 189             = taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions);
 190     private PrintWriter log;
 191 
 192     void setLog(PrintWriter out, PrintWriter err) {
 193         log = out;
 194         taskHelper.setLog(log);
 195     }
 196 
 197     /**
 198      * Result codes.
 199      */
 200     static final int
 201             EXIT_OK = 0, // Completed with no errors.
 202             EXIT_ERROR = 1, // Completed but reported errors.
 203             EXIT_CMDERR = 2, // Bad command-line arguments
 204             EXIT_SYSERR = 3, // System error or resource exhaustion.
 205             EXIT_ABNORMAL = 4;// terminated abnormally
 206 
 207     static class OptionsValues {
 208         boolean help;
 209         String  saveoptsfile;
 210         boolean verbose;
 211         boolean version;
 212         boolean fullVersion;
 213         final List<Path> modulePath = new ArrayList<>();
 214         final Set<String> limitMods = new HashSet<>();
 215         final Set<String> addMods = new HashSet<>();
 216         Path output;
 217         final Map<String, String> launchers = new HashMap<>();
 218         Path packagedModulesPath;
 219         ByteOrder endian = ByteOrder.nativeOrder();
 220         boolean ignoreSigning = false;
 221         boolean bindServices = false;
 222         boolean suggestProviders = false;
 223     }
 224 
 225     int run(String[] args) {
 226         if (log == null) {
 227             setLog(new PrintWriter(System.out, true),
 228                    new PrintWriter(System.err, true));
 229         }
 230         try {
 231             List<String> remaining = optionsHelper.handleOptions(this, args);
 232             if (remaining.size() > 0 && !options.suggestProviders) {
 233                 throw taskHelper.newBadArgs("err.orphan.arguments", toString(remaining))
 234                                 .showUsage(true);
 235             }
 236             if (options.help) {
 237                 optionsHelper.showHelp(PROGNAME);
 238                 return EXIT_OK;
 239             }
 240             if (optionsHelper.shouldListPlugins()) {
 241                 optionsHelper.listPlugins();
 242                 return EXIT_OK;
 243             }
 244             if (options.version || options.fullVersion) {
 245                 taskHelper.showVersion(options.fullVersion);
 246                 return EXIT_OK;
 247             }
 248 
 249             if (taskHelper.getExistingImage() != null) {
 250                 postProcessOnly(taskHelper.getExistingImage());
 251                 return EXIT_OK;
 252             }
 253 
 254 
 255             if (options.modulePath.isEmpty()) {
 256                 // no --module-path specified - try to set $JAVA_HOME/jmods if that exists
 257                 Path jmods = getDefaultModulePath();
 258                 if (jmods != null) {
 259                     options.modulePath.add(jmods);
 260                 }
 261 
 262                 if (options.modulePath.isEmpty()) {
 263                      throw taskHelper.newBadArgs("err.modulepath.must.be.specified")
 264                                  .showUsage(true);
 265                 }
 266             }
 267 
 268             JlinkConfiguration config = initJlinkConfig();
 269             if (options.suggestProviders) {
 270                 suggestProviders(config, remaining);
 271             } else {
 272                 createImage(config);
 273                 if (options.saveoptsfile != null) {
 274                     Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes());
 275                 }
 276             }
 277 
 278             return EXIT_OK;
 279         } catch (PluginException | IllegalArgumentException |
 280                  UncheckedIOException |IOException | FindException | ResolutionException e) {
 281             log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
 282             if (DEBUG) {
 283                 e.printStackTrace(log);
 284             }
 285             return EXIT_ERROR;
 286         } catch (BadArgs e) {
 287             taskHelper.reportError(e.key, e.args);
 288             if (e.showUsage) {
 289                 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME));
 290             }
 291             if (DEBUG) {
 292                 e.printStackTrace(log);
 293             }
 294             return EXIT_CMDERR;
 295         } catch (Throwable x) {
 296             log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage());
 297             x.printStackTrace(log);
 298             return EXIT_ABNORMAL;
 299         } finally {
 300             log.flush();
 301         }
 302     }
 303 
 304     /*
 305      * Jlink API entry point.
 306      */
 307     public static void createImage(JlinkConfiguration config,
 308                                    PluginsConfiguration plugins)
 309             throws Exception {
 310         Objects.requireNonNull(config);
 311         Objects.requireNonNull(config.getOutput());
 312         plugins = plugins == null ? new PluginsConfiguration() : plugins;
 313 
 314         // First create the image provider
 315         ImageProvider imageProvider =
 316                 createImageProvider(config,
 317                                     null,
 318                                     IGNORE_SIGNING_DEFAULT,
 319                                     false,
 320                                     false,
 321                                     null);
 322 
 323         // Then create the Plugin Stack
 324         ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
 325 
 326         //Ask the stack to proceed;
 327         stack.operate(imageProvider);
 328     }
 329 
 330     /*
 331      * Jlink API entry point.
 332      */
 333     public static void postProcessImage(ExecutableImage image, List<Plugin> postProcessorPlugins)
 334             throws Exception {
 335         Objects.requireNonNull(image);
 336         Objects.requireNonNull(postProcessorPlugins);
 337         PluginsConfiguration config = new PluginsConfiguration(postProcessorPlugins);
 338         ImagePluginStack stack = ImagePluginConfiguration.
 339                 parseConfiguration(config);
 340 
 341         stack.operate((ImagePluginStack stack1) -> image);
 342     }
 343 
 344     private void postProcessOnly(Path existingImage) throws Exception {
 345         PluginsConfiguration config = taskHelper.getPluginsConfig(null, null);
 346         ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage);
 347         if (img == null) {
 348             throw taskHelper.newBadArgs("err.existing.image.invalid");
 349         }
 350         postProcessImage(img, config.getPlugins());
 351     }
 352 
 353     // the token for "all modules on the module path"
 354     private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
 355     private JlinkConfiguration initJlinkConfig() throws BadArgs {
 356         Set<String> roots = new HashSet<>();
 357         for (String mod : options.addMods) {
 358             if (mod.equals(ALL_MODULE_PATH)) {
 359                 ModuleFinder finder = newModuleFinder(options.modulePath, options.limitMods, Set.of());
 360                 // all observable modules are roots
 361                 finder.findAll()
 362                       .stream()
 363                       .map(ModuleReference::descriptor)
 364                       .map(ModuleDescriptor::name)
 365                       .forEach(mn -> roots.add(mn));
 366             } else {
 367                 roots.add(mod);
 368             }
 369         }
 370 
 371         ModuleFinder finder = newModuleFinder(options.modulePath, options.limitMods, roots);
 372         if (!finder.find("java.base").isPresent()) {
 373             Path defModPath = getDefaultModulePath();
 374             if (defModPath != null) {
 375                 options.modulePath.add(defModPath);
 376             }
 377             finder = newModuleFinder(options.modulePath, options.limitMods, roots);
 378         }
 379 
 380         return new JlinkConfiguration(options.output,
 381                                       roots,
 382                                       options.endian,
 383                                       finder);
 384     }
 385 
 386     private void createImage(JlinkConfiguration config) throws Exception {
 387         if (options.output == null) {
 388             throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true);
 389         }
 390         if (options.addMods.isEmpty()) {
 391             throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules")
 392                             .showUsage(true);
 393         }
 394 
 395         // First create the image provider
 396         ImageProvider imageProvider = createImageProvider(config,
 397                                                           options.packagedModulesPath,
 398                                                           options.ignoreSigning,
 399                                                           options.bindServices,
 400                                                           options.verbose,
 401                                                           log);
 402 
 403         // Then create the Plugin Stack
 404         ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
 405             taskHelper.getPluginsConfig(options.output, options.launchers));
 406 
 407         //Ask the stack to proceed
 408         stack.operate(imageProvider);
 409     }
 410 
 411     /**
 412      * @return the system module path or null
 413      */
 414     public static Path getDefaultModulePath() {
 415         Path jmods = Paths.get(System.getProperty("java.home"), "jmods");
 416         return Files.isDirectory(jmods)? jmods : null;
 417     }
 418 
 419     /*
 420      * Returns a module finder of the given module path that limits
 421      * the observable modules to those in the transitive closure of
 422      * the modules specified in {@code limitMods} plus other modules
 423      * specified in the {@code roots} set.
 424      *
 425      * @throws IllegalArgumentException if java.base module is present
 426      * but its descriptor has no version
 427      */
 428     public static ModuleFinder newModuleFinder(List<Path> paths,
 429                                                Set<String> limitMods,
 430                                                Set<String> roots)
 431     {
 432         if (Objects.requireNonNull(paths).isEmpty()) {
 433              throw new IllegalArgumentException(taskHelper.getMessage("err.empty.module.path"));
 434         }
 435 
 436         Path[] entries = paths.toArray(new Path[0]);
 437         Runtime.Version version = Runtime.version();
 438         ModuleFinder finder = ModulePath.of(version, true, entries);
 439 
 440         if (finder.find("java.base").isPresent()) {
 441             // use the version of java.base module, if present, as
 442             // the release version for multi-release JAR files
 443             ModuleDescriptor.Version v = finder.find("java.base").get()
 444                 .descriptor().version().orElseThrow(() ->
 445                     new IllegalArgumentException("No version in java.base descriptor")
 446                 );
 447 
 448             // java.base version is different than the current runtime version
 449             version = Runtime.Version.parse(v.toString());
 450             if (Runtime.version().major() != version.major() ||
 451                 Runtime.version().minor() != version.minor()) {
 452                 // jlink version and java.base version do not match.
 453                 // We do not (yet) support this mode.
 454                 throw new IllegalArgumentException(taskHelper.getMessage("err.jlink.version.mismatch",
 455                     Runtime.version().major(), Runtime.version().minor(),
 456                     version.major(), version.minor()));
 457             }
 458         }
 459 
 460         // if limitmods is specified then limit the universe
 461         if (limitMods != null && !limitMods.isEmpty()) {
 462             finder = limitFinder(finder, limitMods, Objects.requireNonNull(roots));
 463         }
 464         return finder;
 465     }
 466 
 467     private static Path toPathLocation(ResolvedModule m) {
 468         Optional<URI> ouri = m.reference().location();
 469         if (!ouri.isPresent())
 470             throw new InternalError(m + " does not have a location");
 471         URI uri = ouri.get();
 472         return Paths.get(uri);
 473     }
 474 
 475 
 476     private static ImageProvider createImageProvider(JlinkConfiguration config,
 477                                                      Path retainModulesPath,
 478                                                      boolean ignoreSigning,
 479                                                      boolean bindService,
 480                                                      boolean verbose,
 481                                                      PrintWriter log)
 482             throws IOException
 483     {
 484         Configuration cf = bindService ? config.resolveAndBind()
 485                                        : config.resolve();
 486 
 487         cf.modules().stream()
 488             .map(ResolvedModule::reference)
 489             .filter(mref -> mref.descriptor().isAutomatic())
 490             .findAny()
 491             .ifPresent(mref -> {
 492                 String loc = mref.location().map(URI::toString).orElse("<unknown>");
 493                 throw new IllegalArgumentException(
 494                     taskHelper.getMessage("err.automatic.module", mref.descriptor().name(), loc));
 495             });
 496 
 497         if (verbose && log != null) {
 498             // print modules to be linked in
 499             cf.modules().stream()
 500               .sorted(Comparator.comparing(ResolvedModule::name))
 501               .forEach(rm -> log.format("%s %s%n",
 502                                         rm.name(), rm.reference().location().get()));
 503 
 504             // print provider info
 505             Set<ModuleReference> references = cf.modules().stream()
 506                 .map(ResolvedModule::reference).collect(Collectors.toSet());
 507 
 508             String msg = String.format("%n%s:", taskHelper.getMessage("providers.header"));
 509             printProviders(log, msg, references);
 510         }
 511 
 512         // emit a warning for any incubating modules in the configuration
 513         if (log != null) {
 514             String im = cf.modules()
 515                           .stream()
 516                           .map(ResolvedModule::reference)
 517                           .filter(ModuleResolution::hasIncubatingWarning)
 518                           .map(ModuleReference::descriptor)
 519                           .map(ModuleDescriptor::name)
 520                           .collect(Collectors.joining(", "));
 521 
 522             if (!"".equals(im))
 523                 log.println("WARNING: Using incubator modules: " + im);
 524         }
 525 
 526         Map<String, Path> mods = cf.modules().stream()
 527             .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation));
 528         return new ImageHelper(cf, mods, config.getByteOrder(), retainModulesPath, ignoreSigning);
 529     }
 530 
 531     /*
 532      * Returns a ModuleFinder that limits observability to the given root
 533      * modules, their transitive dependences, plus a set of other modules.
 534      */
 535     public static ModuleFinder limitFinder(ModuleFinder finder,
 536                                            Set<String> roots,
 537                                            Set<String> otherMods) {
 538 
 539         // resolve all root modules
 540         Configuration cf = Configuration.empty()
 541                 .resolve(finder,
 542                          ModuleFinder.of(),
 543                          roots);
 544 
 545         // module name -> reference
 546         Map<String, ModuleReference> map = new HashMap<>();
 547         cf.modules().forEach(m -> {
 548             ModuleReference mref = m.reference();
 549             map.put(mref.descriptor().name(), mref);
 550         });
 551 
 552         // add the other modules
 553         otherMods.stream()
 554             .map(finder::find)
 555             .flatMap(Optional::stream)
 556             .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref));
 557 
 558         // set of modules that are observable
 559         Set<ModuleReference> mrefs = new HashSet<>(map.values());
 560 
 561         return new ModuleFinder() {
 562             @Override
 563             public Optional<ModuleReference> find(String name) {
 564                 return Optional.ofNullable(map.get(name));
 565             }
 566 
 567             @Override
 568             public Set<ModuleReference> findAll() {
 569                 return mrefs;
 570             }
 571         };
 572     }
 573 
 574     /*
 575      * Returns a map of each service type to the modules that use it
 576      * It will include services that are provided by a module but may not used
 577      * by any of the observable modules.
 578      */
 579     private static Map<String, Set<String>> uses(Set<ModuleReference> modules) {
 580         // collects the services used by the modules and print uses
 581         Map<String, Set<String>> services = new HashMap<>();
 582         modules.stream()
 583                .map(ModuleReference::descriptor)
 584                .forEach(md -> {
 585                    // include services that may not be used by any observable modules
 586                    md.provides().forEach(p ->
 587                        services.computeIfAbsent(p.service(), _k -> new HashSet<>()));
 588                    md.uses().forEach(s -> services.computeIfAbsent(s, _k -> new HashSet<>())
 589                                                   .add(md.name()));
 590                });
 591         return services;
 592     }
 593 
 594     private static void printProviders(PrintWriter log,
 595                                        String header,
 596                                        Set<ModuleReference> modules) {
 597         printProviders(log, header, modules, uses(modules));
 598     }
 599 
 600     /*
 601      * Prints the providers that are used by the specified services.
 602      *
 603      * The specified services maps a service type name to the modules
 604      * using the service type which may be empty if no observable module uses
 605      * that service.
 606      */
 607     private static void printProviders(PrintWriter log,
 608                                        String header,
 609                                        Set<ModuleReference> modules,
 610                                        Map<String, Set<String>> serviceToUses) {
 611         if (modules.isEmpty())
 612             return;
 613 
 614         // Build a map of a service type to the provider modules
 615         Map<String, Set<ModuleDescriptor>> providers = new HashMap<>();
 616         modules.stream()
 617             .map(ModuleReference::descriptor)
 618             .forEach(md -> {
 619                 md.provides().stream()
 620                   .filter(p -> serviceToUses.containsKey(p.service()))
 621                   .forEach(p -> providers.computeIfAbsent(p.service(), _k -> new HashSet<>())
 622                                          .add(md));
 623             });
 624 
 625         if (!providers.isEmpty()) {
 626             log.println(header);
 627         }
 628 
 629         // print the providers of the service types used by the specified modules
 630         // sorted by the service type name and then provider's module name
 631         providers.entrySet().stream()
 632             .sorted(Map.Entry.comparingByKey())
 633             .forEach(e -> {
 634                 String service = e.getKey();
 635                 e.getValue().stream()
 636                  .sorted(Comparator.comparing(ModuleDescriptor::name))
 637                  .forEach(md ->
 638                      md.provides().stream()
 639                        .filter(p -> p.service().equals(service))
 640                        .forEach(p -> {
 641                            String usedBy;
 642                            if (serviceToUses.get(p.service()).isEmpty()) {
 643                                usedBy = "not used by any observable module";
 644                            } else {
 645                                usedBy = serviceToUses.get(p.service()).stream()
 646                                             .sorted()
 647                                             .collect(Collectors.joining(",", "used by ", ""));
 648                            }
 649                            log.format("  %s provides %s %s%n",
 650                                       md.name(), p.service(), usedBy);
 651                        })
 652                  );
 653             });
 654     }
 655 
 656     private void suggestProviders(JlinkConfiguration config, List<String> args)
 657         throws BadArgs
 658     {
 659         if (args.size() > 1) {
 660             throw taskHelper.newBadArgs("err.orphan.argument",
 661                                         toString(args.subList(1, args.size())))
 662                             .showUsage(true);
 663         }
 664 
 665         if (options.bindServices) {
 666             log.println(taskHelper.getMessage("no.suggested.providers"));
 667             return;
 668         }
 669 
 670         ModuleFinder finder = config.finder();
 671         if (args.isEmpty()) {
 672             // print providers used by the observable modules without service binding
 673             Set<ModuleReference> mrefs = finder.findAll();
 674             // print uses of the modules that would be linked into the image
 675             mrefs.stream()
 676                  .sorted(Comparator.comparing(mref -> mref.descriptor().name()))
 677                  .forEach(mref -> {
 678                      ModuleDescriptor md = mref.descriptor();
 679                      log.format("%s %s%n", md.name(),
 680                                 mref.location().get());
 681                      md.uses().stream().sorted()
 682                        .forEach(s -> log.format("    uses %s%n", s));
 683                  });
 684 
 685             String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
 686             printProviders(log, msg, mrefs, uses(mrefs));
 687 
 688         } else {
 689             // comma-separated service types, if specified
 690             Set<String> names = Stream.of(args.get(0).split(","))
 691                 .collect(Collectors.toSet());
 692             // find the modules that provide the specified service
 693             Set<ModuleReference> mrefs = finder.findAll().stream()
 694                 .filter(mref -> mref.descriptor().provides().stream()
 695                                     .map(ModuleDescriptor.Provides::service)
 696                                     .anyMatch(names::contains))
 697                 .collect(Collectors.toSet());
 698 
 699             // find the modules that uses the specified services
 700             Map<String, Set<String>> uses = new HashMap<>();
 701             names.forEach(s -> uses.computeIfAbsent(s, _k -> new HashSet<>()));
 702             finder.findAll().stream()
 703                   .map(ModuleReference::descriptor)
 704                   .forEach(md -> md.uses().stream()
 705                                    .filter(names::contains)
 706                                    .forEach(s -> uses.get(s).add(md.name())));
 707 
 708             // check if any name given on the command line are not provided by any module
 709             mrefs.stream()
 710                  .flatMap(mref -> mref.descriptor().provides().stream()
 711                                       .map(ModuleDescriptor.Provides::service))
 712                  .forEach(names::remove);
 713             if (!names.isEmpty()) {
 714                 log.println(taskHelper.getMessage("warn.provider.notfound",
 715                                                   toString(names)));
 716             }
 717 
 718             String msg = String.format("%n%s:", taskHelper.getMessage("suggested.providers.header"));
 719             printProviders(log, msg, mrefs, uses);
 720         }
 721     }
 722 
 723     private static String toString(Collection<String> collection) {
 724         return collection.stream().sorted()
 725                          .collect(Collectors.joining(","));
 726     }
 727 
 728     private String getSaveOpts() {
 729         StringBuilder sb = new StringBuilder();
 730         sb.append('#').append(new Date()).append("\n");
 731         for (String c : optionsHelper.getInputCommand()) {
 732             sb.append(c).append(" ");
 733         }
 734 
 735         return sb.toString();
 736     }
 737 
 738     private static String getBomHeader() {
 739         StringBuilder sb = new StringBuilder();
 740         sb.append("#").append(new Date()).append("\n");
 741         sb.append("#Please DO NOT Modify this file").append("\n");
 742         return sb.toString();
 743     }
 744 
 745     private String genBOMContent() throws IOException {
 746         StringBuilder sb = new StringBuilder();
 747         sb.append(getBomHeader());
 748         StringBuilder command = new StringBuilder();
 749         for (String c : optionsHelper.getInputCommand()) {
 750             command.append(c).append(" ");
 751         }
 752         sb.append("command").append(" = ").append(command);
 753         sb.append("\n");
 754 
 755         return sb.toString();
 756     }
 757 
 758     private static String genBOMContent(JlinkConfiguration config,
 759             PluginsConfiguration plugins)
 760             throws IOException {
 761         StringBuilder sb = new StringBuilder();
 762         sb.append(getBomHeader());
 763         sb.append(config);
 764         sb.append(plugins);
 765         return sb.toString();
 766     }
 767 
 768     private static class ImageHelper implements ImageProvider {
 769         final ByteOrder order;
 770         final Path packagedModulesPath;
 771         final boolean ignoreSigning;
 772         final Runtime.Version version;
 773         final Set<Archive> archives;
 774 
 775         ImageHelper(Configuration cf,
 776                     Map<String, Path> modsPaths,
 777                     ByteOrder order,
 778                     Path packagedModulesPath,
 779                     boolean ignoreSigning) throws IOException {
 780             this.order = order;
 781             this.packagedModulesPath = packagedModulesPath;
 782             this.ignoreSigning = ignoreSigning;
 783 
 784             // use the version of java.base module, if present, as
 785             // the release version for multi-release JAR files
 786             this.version = cf.findModule("java.base")
 787                 .map(ResolvedModule::reference)
 788                 .map(ModuleReference::descriptor)
 789                 .flatMap(ModuleDescriptor::version)
 790                 .map(ModuleDescriptor.Version::toString)
 791                 .map(Runtime.Version::parse)
 792                 .orElse(Runtime.version());
 793 
 794             this.archives = modsPaths.entrySet().stream()
 795                                 .map(e -> newArchive(e.getKey(), e.getValue()))
 796                                 .collect(Collectors.toSet());
 797         }
 798 
 799         private Archive newArchive(String module, Path path) {
 800             if (path.toString().endsWith(".jmod")) {
 801                 return new JmodArchive(module, path);
 802             } else if (path.toString().endsWith(".jar")) {
 803                 ModularJarArchive modularJarArchive = new ModularJarArchive(module, path, version);
 804 
 805                 Stream<Archive.Entry> signatures = modularJarArchive.entries().filter((entry) -> {
 806                     String name = entry.name().toUpperCase(Locale.ENGLISH);
 807 
 808                     return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && (
 809                                 name.endsWith(".SF") ||
 810                                 name.endsWith(".DSA") ||
 811                                 name.endsWith(".RSA") ||
 812                                 name.endsWith(".EC") ||
 813                                 name.startsWith("META-INF/SIG-")
 814                             );
 815                 });
 816 
 817                 if (signatures.count() != 0) {
 818                     if (ignoreSigning) {
 819                         System.err.println(taskHelper.getMessage("warn.signing", path));
 820                     } else {
 821                         throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path));
 822                     }
 823                 }
 824 
 825                 return modularJarArchive;
 826             } else if (Files.isDirectory(path)) {
 827                 return new DirArchive(path);
 828             } else {
 829                 throw new IllegalArgumentException(
 830                     taskHelper.getMessage("err.not.modular.format", module, path));
 831             }
 832         }
 833 
 834         @Override
 835         public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
 836             ExecutableImage image = ImageFileCreator.create(archives, order, stack);
 837             if (packagedModulesPath != null) {
 838                 // copy the packaged modules to the given path
 839                 Files.createDirectories(packagedModulesPath);
 840                 for (Archive a : archives) {
 841                     Path file = a.getPath();
 842                     Path dest = packagedModulesPath.resolve(file.getFileName());
 843                     Files.copy(file, dest);
 844                 }
 845             }
 846             return image;
 847         }
 848     }
 849 }