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