1 /*
   2  * Copyright (c) 2015, 2016, 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.lang.module.Configuration;
  31 import java.lang.module.ModuleFinder;
  32 import java.lang.reflect.Layer;
  33 import java.nio.file.Files;
  34 import java.nio.file.Path;
  35 import java.nio.file.Paths;
  36 import java.text.MessageFormat;
  37 import java.util.ArrayList;
  38 import java.util.Arrays;
  39 import java.util.Collections;
  40 import java.util.HashMap;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Locale;
  44 import java.util.Map;
  45 import java.util.Map.Entry;
  46 import java.util.MissingResourceException;
  47 import java.util.ResourceBundle;
  48 import java.util.Set;
  49 import java.util.stream.Stream;
  50 
  51 import jdk.tools.jlink.internal.plugins.ExcludeJmodSectionPlugin;
  52 import jdk.tools.jlink.plugin.Plugin;
  53 import jdk.tools.jlink.plugin.Plugin.Category;
  54 import jdk.tools.jlink.builder.DefaultImageBuilder;
  55 import jdk.tools.jlink.builder.ImageBuilder;
  56 import jdk.tools.jlink.plugin.PluginException;
  57 import jdk.tools.jlink.internal.Jlink.PluginsConfiguration;
  58 import jdk.tools.jlink.internal.plugins.PluginsResourceBundle;
  59 import jdk.tools.jlink.internal.plugins.DefaultCompressPlugin;
  60 import jdk.tools.jlink.internal.plugins.StripDebugPlugin;
  61 import jdk.internal.module.ModulePath;
  62 
  63 /**
  64  *
  65  * JLink and JImage tools shared helper.
  66  */
  67 public final class TaskHelper {
  68 
  69     public static final String JLINK_BUNDLE = "jdk.tools.jlink.resources.jlink";
  70     public static final String JIMAGE_BUNDLE = "jdk.tools.jimage.resources.jimage";
  71 
  72     private static final String DEFAULTS_PROPERTY = "jdk.jlink.defaults";
  73 
  74     public final class BadArgs extends Exception {
  75 
  76         static final long serialVersionUID = 8765093759964640721L;
  77 
  78         private BadArgs(String key, Object... args) {
  79             super(bundleHelper.getMessage(key, args));
  80             this.key = key;
  81             this.args = args;
  82         }
  83 
  84         public BadArgs showUsage(boolean b) {
  85             showUsage = b;
  86             return this;
  87         }
  88         public final String key;
  89         public final Object[] args;
  90         public boolean showUsage;
  91     }
  92 
  93     public static class Option<T> implements Comparable<T> {
  94         public interface Processing<T> {
  95 
  96             void process(T task, String opt, String arg) throws BadArgs;
  97         }
  98 
  99         final boolean hasArg;
 100         final Processing<T> processing;
 101         final boolean hidden;
 102         final String name;
 103         final String shortname;
 104 
 105         public Option(boolean hasArg, Processing<T> processing, boolean hidden, String name, String shortname) {
 106             if (!name.startsWith("--")) {
 107                 throw new RuntimeException("option name missing --, " + name);
 108             }
 109             if (!shortname.isEmpty() && !shortname.startsWith("-")) {
 110                 throw new RuntimeException("short name missing -, " + shortname);
 111             }
 112 
 113             this.hasArg = hasArg;
 114             this.processing = processing;
 115             this.hidden = hidden;
 116             this.name = name;
 117             this.shortname = shortname;
 118         }
 119 
 120         public Option(boolean hasArg, Processing<T> processing, String name, String shortname) {
 121             this(hasArg, processing, false, name, shortname);
 122         }
 123 
 124         public Option(boolean hasArg, Processing<T> processing, boolean hidden, String name) {
 125             this(hasArg, processing, hidden, name, "");
 126         }
 127 
 128         public Option(boolean hasArg, Processing<T> processing, String name) {
 129             this(hasArg, processing, false, name, "");
 130         }
 131 
 132         public boolean isHidden() {
 133             return hidden;
 134         }
 135 
 136         public boolean matches(String opt) {
 137             return opt.equals(name) ||
 138                    opt.equals(shortname) ||
 139                    hasArg && opt.startsWith("--") && opt.startsWith(name + "=");
 140          }
 141 
 142         public boolean ignoreRest() {
 143             return false;
 144         }
 145 
 146         void process(T task, String opt, String arg) throws BadArgs {
 147             processing.process(task, opt, arg);
 148         }
 149 
 150         public String getName() {
 151             return name;
 152         }
 153 
 154         public String resourceName() {
 155             return resourcePrefix() + name.substring(2);
 156         }
 157 
 158         public String getShortname() {
 159             return shortname;
 160         }
 161 
 162         public String resourcePrefix() {
 163             return "main.opt.";
 164         }
 165 
 166         @Override
 167         public int compareTo(Object object) {
 168             if (!(object instanceof Option<?>)) {
 169                 throw new RuntimeException("comparing non-Option");
 170             }
 171 
 172             Option<?> option = (Option<?>)object;
 173 
 174             return name.compareTo(option.name);
 175         }
 176 
 177     }
 178 
 179     private static class PluginOption extends Option<PluginsHelper> {
 180         public PluginOption(boolean hasArg,
 181                 Processing<PluginsHelper> processing, boolean hidden, String name, String shortname) {
 182             super(hasArg, processing, hidden, name, shortname);
 183         }
 184 
 185         public PluginOption(boolean hasArg,
 186                 Processing<PluginsHelper> processing, boolean hidden, String name) {
 187             super(hasArg, processing, hidden, name, "");
 188         }
 189 
 190         public String resourcePrefix() {
 191             return "plugin.opt.";
 192         }
 193     }
 194 
 195     private final class PluginsHelper {
 196 
 197         private Layer pluginsLayer = Layer.boot();
 198         private final List<Plugin> plugins;
 199         private String lastSorter;
 200         private boolean listPlugins;
 201         private Path existingImage;
 202 
 203         // plugin to args maps. Each plugin may be used more than once in command line.
 204         // Each such occurrence results in a Map of arguments. So, there could be multiple
 205         // args maps per plugin instance.
 206         private final Map<Plugin, List<Map<String, String>>> pluginToMaps = new HashMap<>();
 207         private final List<PluginOption> pluginsOptions = new ArrayList<>();
 208         private final List<PluginOption> mainOptions = new ArrayList<>();
 209 
 210         private PluginsHelper(String pp) throws BadArgs {
 211 
 212             if (pp != null) {
 213                 String[] dirs = pp.split(File.pathSeparator);
 214                 List<Path> paths = new ArrayList<>(dirs.length);
 215                 for (String dir : dirs) {
 216                     paths.add(Paths.get(dir));
 217                 }
 218 
 219                 pluginsLayer = createPluginsLayer(paths);
 220             }
 221 
 222             plugins = PluginRepository.getPlugins(pluginsLayer);
 223 
 224             Set<String> optionsSeen = new HashSet<>();
 225             for (Plugin plugin : plugins) {
 226                 if (!Utils.isDisabled(plugin)) {
 227                     addOrderedPluginOptions(plugin, optionsSeen);
 228                 }
 229             }
 230             mainOptions.add(new PluginOption(false,
 231                     (task, opt, arg) -> {
 232                         // This option is handled prior
 233                         // to have the options parsed.
 234                     },
 235                     false, "--plugin-module-path"));
 236             mainOptions.add(new PluginOption(true, (task, opt, arg) -> {
 237                     for (Plugin plugin : plugins) {
 238                         if (plugin.getName().equals(arg)) {
 239                             pluginToMaps.remove(plugin);
 240                             return;
 241                         }
 242                     }
 243                     throw newBadArgs("err.no.such.plugin", arg);
 244                 },
 245                 false, "--disable-plugin"));
 246             mainOptions.add(new PluginOption(true, (task, opt, arg) -> {
 247                 Path path = Paths.get(arg);
 248                 if (!Files.exists(path) || !Files.isDirectory(path)) {
 249                     throw newBadArgs("err.image.must.exist", path);
 250                 }
 251                 existingImage = path.toAbsolutePath();
 252             }, true, "--post-process-path"));
 253             mainOptions.add(new PluginOption(true,
 254                     (task, opt, arg) -> {
 255                         lastSorter = arg;
 256                     },
 257                     true, "--resources-last-sorter"));
 258             mainOptions.add(new PluginOption(false,
 259                     (task, opt, arg) -> {
 260                         listPlugins = true;
 261                     },
 262                     false, "--list-plugins"));
 263         }
 264 
 265         private List<Map<String, String>> argListFor(Plugin plugin) {
 266             List<Map<String, String>> mapList = pluginToMaps.get(plugin);
 267             if (mapList == null) {
 268                 mapList = new ArrayList<>();
 269                 pluginToMaps.put(plugin, mapList);
 270             }
 271             return mapList;
 272         }
 273 
 274         private void addEmptyArgumentMap(Plugin plugin) {
 275             argListFor(plugin).add(Collections.emptyMap());
 276         }
 277 
 278         private Map<String, String> addArgumentMap(Plugin plugin) {
 279             Map<String, String> map = new HashMap<>();
 280             argListFor(plugin).add(map);
 281             return map;
 282         }
 283 
 284         private void addOrderedPluginOptions(Plugin plugin,
 285             Set<String> optionsSeen) throws BadArgs {
 286             String option = plugin.getOption();
 287             if (option == null) {
 288                 return;
 289             }
 290 
 291             // make sure that more than one plugin does not use the same option!
 292             if (optionsSeen.contains(option)) {
 293                 throw new BadArgs("err.plugin.mutiple.options",
 294                         option);
 295             }
 296             optionsSeen.add(option);
 297 
 298             PluginOption plugOption
 299                     = new PluginOption(plugin.hasArguments(),
 300                             (task, opt, arg) -> {
 301                                 if (!Utils.isFunctional(plugin)) {
 302                                     throw newBadArgs("err.provider.not.functional",
 303                                             option);
 304                                 }
 305 
 306                                 if (! plugin.hasArguments()) {
 307                                     addEmptyArgumentMap(plugin);
 308                                     return;
 309                                 }
 310 
 311                                 Map<String, String> m = addArgumentMap(plugin);
 312                                 // handle one or more arguments
 313                                 if (arg.indexOf(':') == -1) {
 314                                     // single argument case
 315                                     m.put(option, arg);
 316                                 } else {
 317                                     // This option can accept more than one arguments
 318                                     // like --option_name=arg_value:arg2=value2:arg3=value3
 319 
 320                                     // ":" followed by word char condition takes care of args that
 321                                     // like Windows absolute paths "C:\foo", "C:/foo" [cygwin] etc.
 322                                     // This enforces that key names start with a word character.
 323                                     String[] args = arg.split(":(?=\\w)", -1);
 324                                     String firstArg = args[0];
 325                                     if (firstArg.isEmpty()) {
 326                                         throw newBadArgs("err.provider.additional.arg.error",
 327                                             option, arg);
 328                                     }
 329                                     m.put(option, firstArg);
 330                                     // process the additional arguments
 331                                     for (int i = 1; i < args.length; i++) {
 332                                         String addArg = args[i];
 333                                         int eqIdx = addArg.indexOf('=');
 334                                         if (eqIdx == -1) {
 335                                             throw newBadArgs("err.provider.additional.arg.error",
 336                                                 option, arg);
 337                                         }
 338 
 339                                         String addArgName = addArg.substring(0, eqIdx);
 340                                         String addArgValue = addArg.substring(eqIdx+1);
 341                                         if (addArgName.isEmpty() || addArgValue.isEmpty()) {
 342                                             throw newBadArgs("err.provider.additional.arg.error",
 343                                                 option, arg);
 344                                         }
 345                                         m.put(addArgName, addArgValue);
 346                                     }
 347                                 }
 348                             },
 349                             false, "--" + option);
 350             pluginsOptions.add(plugOption);
 351 
 352             if (Utils.isFunctional(plugin)) {
 353                 if (Utils.isAutoEnabled(plugin)) {
 354                     addEmptyArgumentMap(plugin);
 355                 }
 356 
 357                 if (plugin instanceof DefaultCompressPlugin) {
 358                     plugOption
 359                         = new PluginOption(false,
 360                             (task, opt, arg) -> {
 361                                 Map<String, String> m = addArgumentMap(plugin);
 362                                 m.put(DefaultCompressPlugin.NAME, DefaultCompressPlugin.LEVEL_2);
 363                             }, false, "--compress", "-c");
 364                     mainOptions.add(plugOption);
 365                 } else if (plugin instanceof StripDebugPlugin) {
 366                     plugOption
 367                         = new PluginOption(false,
 368                             (task, opt, arg) -> {
 369                                 addArgumentMap(plugin);
 370                             }, false, "--strip-debug", "-G");
 371                     mainOptions.add(plugOption);
 372                 } else if (plugin instanceof ExcludeJmodSectionPlugin) {
 373                     plugOption = new PluginOption(false, (task, opt, arg) -> {
 374                             Map<String, String> m = addArgumentMap(plugin);
 375                             m.put(ExcludeJmodSectionPlugin.NAME,
 376                                   ExcludeJmodSectionPlugin.MAN_PAGES);
 377                         }, false, "--no-man-pages");
 378                     mainOptions.add(plugOption);
 379 
 380                     plugOption = new PluginOption(false, (task, opt, arg) -> {
 381                         Map<String, String> m = addArgumentMap(plugin);
 382                         m.put(ExcludeJmodSectionPlugin.NAME,
 383                               ExcludeJmodSectionPlugin.INCLUDE_HEADER_FILES);
 384                     }, false, "--no-header-files");
 385                     mainOptions.add(plugOption);
 386                 }
 387             }
 388         }
 389 
 390         private PluginOption getOption(String name) throws BadArgs {
 391             for (PluginOption o : pluginsOptions) {
 392                 if (o.matches(name)) {
 393                     return o;
 394                 }
 395             }
 396             for (PluginOption o : mainOptions) {
 397                 if (o.matches(name)) {
 398                     return o;
 399                 }
 400             }
 401             return null;
 402         }
 403 
 404         private PluginsConfiguration getPluginsConfig(Path output, Map<String, String> launchers
 405                     ) throws IOException, BadArgs {
 406             if (output != null) {
 407                 if (Files.exists(output)) {
 408                     throw new PluginException(PluginsResourceBundle.
 409                             getMessage("err.dir.already.exits", output));
 410                 }
 411             }
 412 
 413             List<Plugin> pluginsList = new ArrayList<>();
 414             for (Entry<Plugin, List<Map<String, String>>> entry : pluginToMaps.entrySet()) {
 415                 Plugin plugin = entry.getKey();
 416                 List<Map<String, String>> argsMaps = entry.getValue();
 417 
 418                 // same plugin option may be used multiple times in command line.
 419                 // we call configure once for each occurrence. It is upto the plugin
 420                 // to 'merge' and/or 'override' arguments.
 421                 for (Map<String, String> map : argsMaps) {
 422                     try {
 423                         plugin.configure(Collections.unmodifiableMap(map));
 424                     } catch (IllegalArgumentException e) {
 425                         if (JlinkTask.DEBUG) {
 426                             System.err.println("Plugin " + plugin.getName() + " threw exception with config: " + map);
 427                             e.printStackTrace();
 428                         }
 429                         throw e;
 430                     }
 431                 }
 432 
 433                 if (!Utils.isDisabled(plugin)) {
 434                     pluginsList.add(plugin);
 435                 }
 436             }
 437 
 438             // recreate or postprocessing don't require an output directory.
 439             ImageBuilder builder = null;
 440             if (output != null) {
 441                 builder = new DefaultImageBuilder(output, launchers);
 442             }
 443 
 444             return new Jlink.PluginsConfiguration(pluginsList,
 445                     builder, lastSorter);
 446         }
 447     }
 448 
 449     private static final class ResourceBundleHelper {
 450 
 451         private final ResourceBundle bundle;
 452         private final ResourceBundle pluginBundle;
 453 
 454         ResourceBundleHelper(String path) {
 455             Locale locale = Locale.getDefault();
 456             try {
 457                 bundle = ResourceBundle.getBundle(path, locale);
 458                 pluginBundle = ResourceBundle.getBundle("jdk.tools.jlink.resources.plugins", locale);
 459             } catch (MissingResourceException e) {
 460                 throw new InternalError("Cannot find jlink resource bundle for locale " + locale);
 461             }
 462         }
 463 
 464         String getMessage(String key, Object... args) {
 465             String val;
 466             try {
 467                 val = bundle.getString(key);
 468             } catch (MissingResourceException e) {
 469                 // XXX OK, check in plugin bundle
 470                 val = pluginBundle.getString(key);
 471             }
 472             return MessageFormat.format(val, args);
 473         }
 474 
 475     }
 476 
 477     public final class OptionsHelper<T> {
 478 
 479         private final List<Option<T>> options;
 480         private String[] command;
 481         private String defaults;
 482 
 483         OptionsHelper(List<Option<T>> options) {
 484             this.options = options;
 485         }
 486 
 487         private boolean hasArgument(String optionName) throws BadArgs {
 488             Option<?> opt = getOption(optionName);
 489             if (opt == null) {
 490                 opt = pluginOptions.getOption(optionName);
 491                 if (opt == null) {
 492                     throw new BadArgs("err.unknown.option", optionName).
 493                             showUsage(true);
 494                 }
 495             }
 496             return opt.hasArg;
 497         }
 498 
 499         public boolean shouldListPlugins() {
 500             return pluginOptions.listPlugins;
 501         }
 502 
 503         private String getPluginsPath(String[] args) throws BadArgs {
 504             return null;
 505         }
 506 
 507         // used by jimage. Return unhandled arguments like "create", "describe".
 508         public List<String> handleOptions(T task, String[] args) throws BadArgs {
 509             return handleOptions(task, args, true);
 510         }
 511 
 512         // used by jlink. No unhandled arguments like "create", "describe".
 513         void handleOptionsNoUnhandled(T task, String[] args) throws BadArgs {
 514             handleOptions(task, args, false);
 515         }
 516 
 517         // shared code that handles options for both jlink and jimage. jimage uses arguments like
 518         // "create", "describe" etc. as "task names". Those arguments are unhandled here and returned
 519         // as "unhandled arguments list". jlink does not want such arguments. "collectUnhandled" flag
 520         // tells whether to allow for unhandled arguments or not.
 521         private List<String> handleOptions(T task, String[] args, boolean collectUnhandled) throws BadArgs {
 522             // findbugs warning, copy instead of keeping a reference.
 523             command = Arrays.copyOf(args, args.length);
 524 
 525             // Must extract it prior to do any option analysis.
 526             // Required to interpret custom plugin options.
 527             // Unit tests can call Task multiple time in same JVM.
 528             pluginOptions = new PluginsHelper(null);
 529 
 530             List<String> rest = collectUnhandled? new ArrayList<>() : null;
 531             // process options
 532             for (int i = 0; i < args.length; i++) {
 533                 if (args[i].charAt(0) == '-') {
 534                     String name = args[i];
 535                     PluginOption pluginOption = null;
 536                     Option<T> option = getOption(name);
 537                     if (option == null) {
 538                         pluginOption = pluginOptions.getOption(name);
 539                         if (pluginOption == null) {
 540 
 541                             throw new BadArgs("err.unknown.option", name).
 542                                     showUsage(true);
 543                         }
 544                     }
 545                     Option<?> opt = pluginOption == null ? option : pluginOption;
 546                     String param = null;
 547                     if (opt.hasArg) {
 548                         if (name.startsWith("--") && name.indexOf('=') > 0) {
 549                             param = name.substring(name.indexOf('=') + 1,
 550                                     name.length());
 551                         } else if (i + 1 < args.length) {
 552                             param = args[++i];
 553                         }
 554                         if (param == null || param.isEmpty()
 555                                 || (param.length() >= 2 && param.charAt(0) == '-'
 556                                 && param.charAt(1) == '-')) {
 557                             throw new BadArgs("err.missing.arg", name).
 558                                     showUsage(true);
 559                         }
 560                     }
 561                     if (pluginOption != null) {
 562                         pluginOption.process(pluginOptions, name, param);
 563                     } else {
 564                         option.process(task, name, param);
 565                     }
 566                     if (opt.ignoreRest()) {
 567                         i = args.length;
 568                     }
 569                 } else {
 570                     if (collectUnhandled) {
 571                         rest.add(args[i]);
 572                     } else {
 573                         throw new BadArgs("err.orphan.argument", args[i]).
 574                             showUsage(true);
 575                     }
 576                 }
 577             }
 578             return rest;
 579         }
 580 
 581         private Option<T> getOption(String name) {
 582             for (Option<T> o : options) {
 583                 if (o.matches(name)) {
 584                     return o;
 585                 }
 586             }
 587             return null;
 588         }
 589 
 590         public void showHelp(String progName) {
 591             log.println(bundleHelper.getMessage("main.usage", progName));
 592             Stream.concat(options.stream(), pluginOptions.mainOptions.stream())
 593                 .filter(option -> !option.isHidden())
 594                 .sorted()
 595                 .forEach(option -> {
 596                      log.println(bundleHelper.getMessage(option.resourceName()));
 597                 });
 598 
 599             log.println(bundleHelper.getMessage("main.command.files"));
 600         }
 601 
 602         public void listPlugins() {
 603             log.println("\n" + bundleHelper.getMessage("main.extended.help"));
 604             List<Plugin> pluginList = PluginRepository.
 605                     getPlugins(pluginOptions.pluginsLayer);
 606 
 607             for (Plugin plugin : Utils.getSortedPlugins(pluginList)) {
 608                 showPlugin(plugin, log);
 609             }
 610 
 611             log.println("\n" + bundleHelper.getMessage("main.extended.help.footer"));
 612         }
 613 
 614         private void showPlugin(Plugin plugin, PrintWriter log) {
 615             if (showsPlugin(plugin)) {
 616                 log.println("\n" + bundleHelper.getMessage("main.plugin.name")
 617                         + ": " + plugin.getName());
 618 
 619                 // print verbose details for non-builtin plugins
 620                 if (!Utils.isBuiltin(plugin)) {
 621                     log.println(bundleHelper.getMessage("main.plugin.class")
 622                          + ": " + plugin.getClass().getName());
 623                     log.println(bundleHelper.getMessage("main.plugin.module")
 624                          + ": " + plugin.getClass().getModule().getName());
 625                     Category category = plugin.getType();
 626                     log.println(bundleHelper.getMessage("main.plugin.category")
 627                          + ": " + category.getName());
 628                     log.println(bundleHelper.getMessage("main.plugin.state")
 629                         + ": " + plugin.getStateDescription());
 630                 }
 631 
 632                 String option = plugin.getOption();
 633                 if (option != null) {
 634                     log.println(bundleHelper.getMessage("main.plugin.option")
 635                         + ": --" + plugin.getOption()
 636                         + (plugin.hasArguments()? ("=" + plugin.getArgumentsDescription()) : ""));
 637                 }
 638 
 639                 // description can be long spanning more than one line and so
 640                 // print a newline after description label.
 641                 log.println(bundleHelper.getMessage("main.plugin.description")
 642                         + ": " + plugin.getDescription());
 643             }
 644         }
 645 
 646         String[] getInputCommand() {
 647             return command;
 648         }
 649 
 650         String getDefaults() {
 651             return defaults;
 652         }
 653 
 654         public Layer getPluginsLayer() {
 655             return pluginOptions.pluginsLayer;
 656         }
 657     }
 658 
 659     private PluginsHelper pluginOptions;
 660     private PrintWriter log;
 661     private final ResourceBundleHelper bundleHelper;
 662 
 663     public TaskHelper(String path) {
 664         if (!JLINK_BUNDLE.equals(path) && !JIMAGE_BUNDLE.equals(path)) {
 665             throw new IllegalArgumentException("Invalid Bundle");
 666         }
 667         this.bundleHelper = new ResourceBundleHelper(path);
 668     }
 669 
 670     public <T> OptionsHelper<T> newOptionsHelper(Class<T> clazz,
 671             Option<?>[] options) {
 672         List<Option<T>> optionsList = new ArrayList<>();
 673         for (Option<?> o : options) {
 674             @SuppressWarnings("unchecked")
 675             Option<T> opt = (Option<T>) o;
 676             optionsList.add(opt);
 677         }
 678         return new OptionsHelper<>(optionsList);
 679     }
 680 
 681     public BadArgs newBadArgs(String key, Object... args) {
 682         return new BadArgs(key, args);
 683     }
 684 
 685     public String getMessage(String key, Object... args) {
 686         return bundleHelper.getMessage(key, args);
 687     }
 688 
 689     public void setLog(PrintWriter log) {
 690         this.log = log;
 691     }
 692 
 693     public void reportError(String key, Object... args) {
 694         log.println(bundleHelper.getMessage("error.prefix") + " "
 695                 + bundleHelper.getMessage(key, args));
 696     }
 697 
 698     public void reportUnknownError(String message) {
 699         log.println(bundleHelper.getMessage("error.prefix") + " " + message);
 700     }
 701 
 702     public void warning(String key, Object... args) {
 703         log.println(bundleHelper.getMessage("warn.prefix") + " "
 704                 + bundleHelper.getMessage(key, args));
 705     }
 706 
 707     public PluginsConfiguration getPluginsConfig(Path output, Map<String, String> launchers)
 708             throws IOException, BadArgs {
 709         return pluginOptions.getPluginsConfig(output, launchers);
 710     }
 711 
 712     public Path getExistingImage() {
 713         return pluginOptions.existingImage;
 714     }
 715 
 716     public void showVersion(boolean full) {
 717         log.println(version(full ? "full" : "release"));
 718     }
 719 
 720     public String version(String key) {
 721         return System.getProperty("java.version");
 722     }
 723 
 724     static Layer createPluginsLayer(List<Path> paths) {
 725 
 726         Path[] dirs = paths.toArray(new Path[0]);
 727         ModuleFinder finder = new ModulePath(Runtime.version(), true, dirs);
 728         Configuration bootConfiguration = Layer.boot().configuration();
 729         try {
 730             Configuration cf = bootConfiguration
 731                 .resolveRequiresAndUses(ModuleFinder.of(),
 732                                         finder,
 733                                         Collections.emptySet());
 734             ClassLoader scl = ClassLoader.getSystemClassLoader();
 735             return Layer.boot().defineModulesWithOneLoader(cf, scl);
 736         } catch (Exception ex) {
 737             // Malformed plugin modules (e.g.: same package in multiple modules).
 738             throw new PluginException("Invalid modules in the plugins path: " + ex);
 739         }
 740     }
 741 
 742     // Display all plugins
 743     private static boolean showsPlugin(Plugin plugin) {
 744         return (!Utils.isDisabled(plugin) && plugin.getOption() != null);
 745     }
 746 }