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