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.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.*;
  44 import java.util.stream.Collectors;
  45 import java.util.stream.Stream;
  46 
  47 import jdk.tools.jlink.internal.TaskHelper.BadArgs;
  48 import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE;
  49 import jdk.tools.jlink.internal.Jlink.JlinkConfiguration;
  50 import jdk.tools.jlink.internal.Jlink.PluginsConfiguration;
  51 import jdk.tools.jlink.internal.TaskHelper.Option;
  52 import jdk.tools.jlink.internal.TaskHelper.OptionsHelper;
  53 import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider;
  54 import jdk.tools.jlink.plugin.PluginException;
  55 import jdk.tools.jlink.builder.DefaultImageBuilder;
  56 import jdk.tools.jlink.plugin.Plugin;
  57 import jdk.internal.module.ModulePath;
  58 import jdk.internal.module.ModuleResolution;
  59 
  60 /**
  61  * Implementation for the jlink tool.
  62  *
  63  * ## Should use jdk.joptsimple some day.
  64  */
  65 public class JlinkTask {
  66     static final boolean DEBUG = Boolean.getBoolean("jlink.debug");
  67 
  68     // jlink API ignores by default. Remove when signing is implemented.
  69     static final boolean IGNORE_SIGNING_DEFAULT = true;
  70 
  71     private static final TaskHelper taskHelper
  72             = new TaskHelper(JLINK_BUNDLE);
  73 
  74     private static final Option<?>[] recognizedOptions = {
  75         new Option<JlinkTask>(false, (task, opt, arg) -> {
  76             task.options.help = true;
  77         }, "--help", "-h"),
  78         new Option<JlinkTask>(true, (task, opt, arg) -> {
  79             // if used multiple times, the last one wins!
  80             // So, clear previous values, if any.
  81             task.options.modulePath.clear();
  82             String[] dirs = arg.split(File.pathSeparator);
  83             int i = 0;
  84             Arrays.stream(dirs)
  85                   .map(Paths::get)
  86                   .forEach(task.options.modulePath::add);
  87         }, "--module-path", "-p"),
  88         new Option<JlinkTask>(true, (task, opt, arg) -> {
  89             // if used multiple times, the last one wins!
  90             // So, clear previous values, if any.
  91             task.options.limitMods.clear();
  92             for (String mn : arg.split(",")) {
  93                 if (mn.isEmpty()) {
  94                     throw taskHelper.newBadArgs("err.mods.must.be.specified",
  95                             "--limit-modules");
  96                 }
  97                 task.options.limitMods.add(mn);
  98             }
  99         }, "--limit-modules"),
 100         new Option<JlinkTask>(true, (task, opt, arg) -> {
 101             for (String mn : arg.split(",")) {
 102                 if (mn.isEmpty()) {
 103                     throw taskHelper.newBadArgs("err.mods.must.be.specified",
 104                             "--add-modules");
 105                 }
 106                 task.options.addMods.add(mn);
 107             }
 108         }, "--add-modules"),
 109         new Option<JlinkTask>(true, (task, opt, arg) -> {
 110             Path path = Paths.get(arg);
 111             task.options.output = path;
 112         }, "--output"),
 113         new Option<JlinkTask>(true, (task, opt, arg) -> {
 114             String[] values = arg.split("=");
 115             // check values
 116             if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) {
 117                 throw taskHelper.newBadArgs("err.launcher.value.format", arg);
 118             } else {
 119                 String commandName = values[0];
 120                 String moduleAndMain = values[1];
 121                 int idx = moduleAndMain.indexOf("/");
 122                 if (idx != -1) {
 123                     if (moduleAndMain.substring(0, idx).isEmpty()) {
 124                         throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg);
 125                     }
 126 
 127                     if (moduleAndMain.substring(idx + 1).isEmpty()) {
 128                         throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg);
 129                     }
 130                 }
 131                 task.options.launchers.put(commandName, moduleAndMain);
 132             }
 133         }, "--launcher"),
 134         new Option<JlinkTask>(true, (task, opt, arg) -> {
 135             if ("little".equals(arg)) {
 136                 task.options.endian = ByteOrder.LITTLE_ENDIAN;
 137             } else if ("big".equals(arg)) {
 138                 task.options.endian = ByteOrder.BIG_ENDIAN;
 139             } else {
 140                 throw taskHelper.newBadArgs("err.unknown.byte.order", arg);
 141             }
 142         }, "--endian"),
 143         new Option<JlinkTask>(false, (task, opt, arg) -> {
 144             task.options.version = true;
 145         }, "--version"),
 146         new Option<JlinkTask>(true, (task, opt, arg) -> {
 147             Path path = Paths.get(arg);
 148             if (Files.exists(path)) {
 149                 throw taskHelper.newBadArgs("err.dir.exists", path);
 150             }
 151             task.options.packagedModulesPath = path;
 152         }, true, "--keep-packaged-modules"),
 153         new Option<JlinkTask>(true, (task, opt, arg) -> {
 154             task.options.saveoptsfile = arg;
 155         }, "--save-opts"),
 156         new Option<JlinkTask>(false, (task, opt, arg) -> {
 157             task.options.fullVersion = true;
 158         }, true, "--full-version"),
 159         new Option<JlinkTask>(false, (task, opt, arg) -> {
 160             task.options.ignoreSigning = true;
 161         }, "--ignore-signing-information"),};
 162 
 163     private static final String PROGNAME = "jlink";
 164     private final OptionsValues options = new OptionsValues();
 165 
 166     private static final OptionsHelper<JlinkTask> optionsHelper
 167             = taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions);
 168     private PrintWriter log;
 169 
 170     void setLog(PrintWriter out, PrintWriter err) {
 171         log = out;
 172         taskHelper.setLog(log);
 173     }
 174 
 175     /**
 176      * Result codes.
 177      */
 178     static final int
 179             EXIT_OK = 0, // Completed with no errors.
 180             EXIT_ERROR = 1, // Completed but reported errors.
 181             EXIT_CMDERR = 2, // Bad command-line arguments
 182             EXIT_SYSERR = 3, // System error or resource exhaustion.
 183             EXIT_ABNORMAL = 4;// terminated abnormally
 184 
 185     static class OptionsValues {
 186         boolean help;
 187         String  saveoptsfile;
 188         boolean version;
 189         boolean fullVersion;
 190         final List<Path> modulePath = new ArrayList<>();
 191         final Set<String> limitMods = new HashSet<>();
 192         final Set<String> addMods = new HashSet<>();
 193         Path output;
 194         final Map<String, String> launchers = new HashMap<>();
 195         Path packagedModulesPath;
 196         ByteOrder endian = ByteOrder.nativeOrder();
 197         boolean ignoreSigning = false;
 198     }
 199 
 200     int run(String[] args) {
 201         if (log == null) {
 202             setLog(new PrintWriter(System.out, true),
 203                    new PrintWriter(System.err, true));
 204         }
 205         try {
 206             optionsHelper.handleOptionsNoUnhandled(this, args);
 207             if (options.help) {
 208                 optionsHelper.showHelp(PROGNAME);
 209                 return EXIT_OK;
 210             }
 211             if (optionsHelper.shouldListPlugins()) {
 212                 optionsHelper.listPlugins();
 213                 return EXIT_OK;
 214             }
 215             if (options.version || options.fullVersion) {
 216                 taskHelper.showVersion(options.fullVersion);
 217                 return EXIT_OK;
 218             }
 219 
 220             if (taskHelper.getExistingImage() == null) {
 221                 if (options.modulePath.isEmpty()) {
 222                     throw taskHelper.newBadArgs("err.modulepath.must.be.specified").showUsage(true);
 223                 }
 224                 createImage();
 225             } else {
 226                 postProcessOnly(taskHelper.getExistingImage());
 227             }
 228 
 229             if (options.saveoptsfile != null) {
 230                 Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes());
 231             }
 232 
 233             return EXIT_OK;
 234         } catch (PluginException | IllegalArgumentException |
 235                  UncheckedIOException |IOException | FindException | ResolutionException e) {
 236             log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage());
 237             if (DEBUG) {
 238                 e.printStackTrace(log);
 239             }
 240             return EXIT_ERROR;
 241         } catch (BadArgs e) {
 242             taskHelper.reportError(e.key, e.args);
 243             if (e.showUsage) {
 244                 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME));
 245             }
 246             if (DEBUG) {
 247                 e.printStackTrace(log);
 248             }
 249             return EXIT_CMDERR;
 250         } catch (Throwable x) {
 251             log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage());
 252             x.printStackTrace(log);
 253             return EXIT_ABNORMAL;
 254         } finally {
 255             log.flush();
 256         }
 257     }
 258 
 259     /*
 260      * Jlink API entry point.
 261      */
 262     public static void createImage(JlinkConfiguration config,
 263                                    PluginsConfiguration plugins)
 264             throws Exception {
 265         Objects.requireNonNull(config);
 266         Objects.requireNonNull(config.getOutput());
 267         plugins = plugins == null ? new PluginsConfiguration() : plugins;
 268 
 269         if (config.getModulepaths().isEmpty()) {
 270             throw new IllegalArgumentException("Empty module paths");
 271         }
 272 
 273         ModuleFinder finder = newModuleFinder(config.getModulepaths(),
 274                                               config.getLimitmods(),
 275                                               config.getModules());
 276 
 277         if (config.getModules().isEmpty()) {
 278             throw new IllegalArgumentException("No modules to add");
 279         }
 280 
 281         // First create the image provider
 282         ImageProvider imageProvider =
 283                 createImageProvider(finder,
 284                                     config.getModules(),
 285                                     config.getByteOrder(),
 286                                     null,
 287                                     IGNORE_SIGNING_DEFAULT,
 288                                     null);
 289 
 290         // Then create the Plugin Stack
 291         ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
 292 
 293         //Ask the stack to proceed;
 294         stack.operate(imageProvider);
 295     }
 296 
 297     /*
 298      * Jlink API entry point.
 299      */
 300     public static void postProcessImage(ExecutableImage image, List<Plugin> postProcessorPlugins)
 301             throws Exception {
 302         Objects.requireNonNull(image);
 303         Objects.requireNonNull(postProcessorPlugins);
 304         PluginsConfiguration config = new PluginsConfiguration(postProcessorPlugins);
 305         ImagePluginStack stack = ImagePluginConfiguration.
 306                 parseConfiguration(config);
 307 
 308         stack.operate((ImagePluginStack stack1) -> image);
 309     }
 310 
 311     private void postProcessOnly(Path existingImage) throws Exception {
 312         PluginsConfiguration config = taskHelper.getPluginsConfig(null, null);
 313         ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage);
 314         if (img == null) {
 315             throw taskHelper.newBadArgs("err.existing.image.invalid");
 316         }
 317         postProcessImage(img, config.getPlugins());
 318     }
 319 
 320     // the token for "all modules on the module path"
 321     private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
 322     private void createImage() throws Exception {
 323         if (options.output == null) {
 324             throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true);
 325         }
 326 
 327         if (options.addMods.isEmpty()) {
 328             throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules")
 329                     .showUsage(true);
 330         }
 331 
 332         Set<String> roots = new HashSet<>();
 333         for (String mod : options.addMods) {
 334             if (mod.equals(ALL_MODULE_PATH)) {
 335                 ModuleFinder finder = modulePathFinder();
 336                 finder.findAll()
 337                       .stream()
 338                       .map(ModuleReference::descriptor)
 339                       .map(ModuleDescriptor::name)
 340                       .forEach(mn -> roots.add(mn));
 341             } else {
 342                 roots.add(mod);
 343             }
 344         }
 345 
 346         ModuleFinder finder = newModuleFinder(options.modulePath,
 347                                               options.limitMods,
 348                                               roots);
 349 
 350 
 351         // First create the image provider
 352         ImageProvider imageProvider = createImageProvider(finder,
 353                                                           roots,
 354                                                           options.endian,
 355                                                           options.packagedModulesPath,
 356                                                           options.ignoreSigning,
 357                                                           log);
 358 
 359         // Then create the Plugin Stack
 360         ImagePluginStack stack = ImagePluginConfiguration.
 361                 parseConfiguration(taskHelper.getPluginsConfig(options.output, options.launchers));
 362 
 363         //Ask the stack to proceed
 364         stack.operate(imageProvider);
 365     }
 366 
 367     /**
 368      * Returns a module finder to find the observable modules specified in
 369      * the --module-path and --limit-modules options
 370      */
 371     private ModuleFinder modulePathFinder() {
 372         Path[] entries = options.modulePath.toArray(new Path[0]);
 373         ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries);
 374         if (!options.limitMods.isEmpty()) {
 375             finder = limitFinder(finder, options.limitMods, Collections.emptySet());
 376         }
 377         return finder;
 378     }
 379 
 380     /*
 381      * Returns a module finder of the given module path that limits
 382      * the observable modules to those in the transitive closure of
 383      * the modules specified in {@code limitMods} plus other modules
 384      * specified in the {@code roots} set.
 385      */
 386     public static ModuleFinder newModuleFinder(List<Path> paths,
 387                                                Set<String> limitMods,
 388                                                Set<String> roots)
 389     {
 390         Path[] entries = paths.toArray(new Path[0]);
 391         ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries);
 392 
 393         // if limitmods is specified then limit the universe
 394         if (!limitMods.isEmpty()) {
 395             finder = limitFinder(finder, limitMods, roots);
 396         }
 397         return finder;
 398     }
 399 
 400     private static Path toPathLocation(ResolvedModule m) {
 401         Optional<URI> ouri = m.reference().location();
 402         if (!ouri.isPresent())
 403             throw new InternalError(m + " does not have a location");
 404         URI uri = ouri.get();
 405         return Paths.get(uri);
 406     }
 407 
 408     private static ImageProvider createImageProvider(ModuleFinder finder,
 409                                                      Set<String> roots,
 410                                                      ByteOrder order,
 411                                                      Path retainModulesPath,
 412                                                      boolean ignoreSigning,
 413                                                      PrintWriter log)
 414             throws IOException
 415     {
 416         if (roots.isEmpty()) {
 417             throw new IllegalArgumentException("empty modules and limitmods");
 418         }
 419 
 420         Configuration cf = Configuration.empty()
 421                 .resolve(finder,
 422                          ModuleFinder.of(),
 423                          roots);
 424 
 425         // emit a warning for any incubating modules in the configuration
 426         if (log != null) {
 427             String im = cf.modules()
 428                           .stream()
 429                           .map(ResolvedModule::reference)
 430                           .filter(ModuleResolution::hasIncubatingWarning)
 431                           .map(ModuleReference::descriptor)
 432                           .map(ModuleDescriptor::name)
 433                           .collect(Collectors.joining(", "));
 434 
 435             if (!"".equals(im))
 436                 log.println("WARNING: Using incubator modules: " + im);
 437         }
 438 
 439         Map<String, Path> mods = cf.modules().stream()
 440             .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation));
 441         return new ImageHelper(cf, mods, order, retainModulesPath, ignoreSigning);
 442     }
 443 
 444     /*
 445      * Returns a ModuleFinder that limits observability to the given root
 446      * modules, their transitive dependences, plus a set of other modules.
 447      */
 448     private static ModuleFinder limitFinder(ModuleFinder finder,
 449                                             Set<String> roots,
 450                                             Set<String> otherMods) {
 451 
 452         // resolve all root modules
 453         Configuration cf = Configuration.empty()
 454                 .resolve(finder,
 455                          ModuleFinder.of(),
 456                          roots);
 457 
 458         // module name -> reference
 459         Map<String, ModuleReference> map = new HashMap<>();
 460         cf.modules().forEach(m -> {
 461             ModuleReference mref = m.reference();
 462             map.put(mref.descriptor().name(), mref);
 463         });
 464 
 465         // add the other modules
 466         otherMods.stream()
 467             .map(finder::find)
 468             .flatMap(Optional::stream)
 469             .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref));
 470 
 471         // set of modules that are observable
 472         Set<ModuleReference> mrefs = new HashSet<>(map.values());
 473 
 474         return new ModuleFinder() {
 475             @Override
 476             public Optional<ModuleReference> find(String name) {
 477                 return Optional.ofNullable(map.get(name));
 478             }
 479 
 480             @Override
 481             public Set<ModuleReference> findAll() {
 482                 return mrefs;
 483             }
 484         };
 485     }
 486 
 487     private String getSaveOpts() {
 488         StringBuilder sb = new StringBuilder();
 489         sb.append('#').append(new Date()).append("\n");
 490         for (String c : optionsHelper.getInputCommand()) {
 491             sb.append(c).append(" ");
 492         }
 493 
 494         return sb.toString();
 495     }
 496 
 497     private static String getBomHeader() {
 498         StringBuilder sb = new StringBuilder();
 499         sb.append("#").append(new Date()).append("\n");
 500         sb.append("#Please DO NOT Modify this file").append("\n");
 501         return sb.toString();
 502     }
 503 
 504     private String genBOMContent() throws IOException {
 505         StringBuilder sb = new StringBuilder();
 506         sb.append(getBomHeader());
 507         StringBuilder command = new StringBuilder();
 508         for (String c : optionsHelper.getInputCommand()) {
 509             command.append(c).append(" ");
 510         }
 511         sb.append("command").append(" = ").append(command);
 512         sb.append("\n");
 513 
 514         return sb.toString();
 515     }
 516 
 517     private static String genBOMContent(JlinkConfiguration config,
 518             PluginsConfiguration plugins)
 519             throws IOException {
 520         StringBuilder sb = new StringBuilder();
 521         sb.append(getBomHeader());
 522         sb.append(config);
 523         sb.append(plugins);
 524         return sb.toString();
 525     }
 526 
 527     private static class ImageHelper implements ImageProvider {
 528         final ByteOrder order;
 529         final Path packagedModulesPath;
 530         final boolean ignoreSigning;
 531         final Set<Archive> archives;
 532 
 533         ImageHelper(Configuration cf,
 534                     Map<String, Path> modsPaths,
 535                     ByteOrder order,
 536                     Path packagedModulesPath,
 537                     boolean ignoreSigning) throws IOException {
 538             this.order = order;
 539             this.packagedModulesPath = packagedModulesPath;
 540             this.ignoreSigning = ignoreSigning;
 541             this.archives = modsPaths.entrySet().stream()
 542                                 .map(e -> newArchive(e.getKey(), e.getValue()))
 543                                 .collect(Collectors.toSet());
 544         }
 545 
 546         private Archive newArchive(String module, Path path) {
 547             if (path.toString().endsWith(".jmod")) {
 548                 return new JmodArchive(module, path);
 549             } else if (path.toString().endsWith(".jar")) {
 550                 ModularJarArchive modularJarArchive = new ModularJarArchive(module, path);
 551 
 552                 Stream<Archive.Entry> signatures = modularJarArchive.entries().filter((entry) -> {
 553                     String name = entry.name().toUpperCase(Locale.ENGLISH);
 554 
 555                     return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && (
 556                                 name.endsWith(".SF") ||
 557                                 name.endsWith(".DSA") ||
 558                                 name.endsWith(".RSA") ||
 559                                 name.endsWith(".EC") ||
 560                                 name.startsWith("META-INF/SIG-")
 561                             );
 562                 });
 563 
 564                 if (signatures.count() != 0) {
 565                     if (ignoreSigning) {
 566                         System.err.println(taskHelper.getMessage("warn.signing", path));
 567                     } else {
 568                         throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path));
 569                     }
 570                 }
 571 
 572                 return modularJarArchive;
 573             } else if (Files.isDirectory(path)) {
 574                 return new DirArchive(path);
 575             } else {
 576                 throw new IllegalArgumentException(
 577                     taskHelper.getMessage("err.not.modular.format", module, path));
 578             }
 579         }
 580 
 581         @Override
 582         public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
 583             ExecutableImage image = ImageFileCreator.create(archives, order, stack);
 584             if (packagedModulesPath != null) {
 585                 // copy the packaged modules to the given path
 586                 Files.createDirectories(packagedModulesPath);
 587                 for (Archive a : archives) {
 588                     Path file = a.getPath();
 589                     Path dest = packagedModulesPath.resolve(file.getFileName());
 590                     Files.copy(file, dest);
 591                 }
 592             }
 593             return image;
 594         }
 595     }
 596 }