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