1 /*
   2  * Copyright (c) 2015, 2019, 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 
  26 package jdk.jpackage.internal;
  27 
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.io.StringReader;
  31 import java.io.PrintWriter;
  32 import java.io.StringWriter;
  33 import java.nio.file.Files;
  34 import java.nio.file.Path;
  35 import java.text.MessageFormat;
  36 import java.util.ArrayList;
  37 import java.util.Collection;
  38 import java.util.Collections;
  39 import java.util.EnumSet;
  40 import java.util.HashMap;
  41 import java.util.HashSet;
  42 import java.util.Iterator;
  43 import java.util.LinkedHashMap;
  44 import java.util.LinkedHashSet;
  45 import java.util.List;
  46 import java.util.Map;
  47 import java.util.Properties;
  48 import java.util.ResourceBundle;
  49 import java.util.Set;
  50 import java.util.Optional;
  51 import java.util.Arrays;
  52 import java.util.stream.Collectors;
  53 import java.util.stream.Stream;
  54 import java.util.regex.Matcher;
  55 import java.util.spi.ToolProvider;
  56 import java.lang.module.Configuration;
  57 import java.lang.module.ResolvedModule;
  58 import java.lang.module.ModuleDescriptor;
  59 import java.lang.module.ModuleFinder;
  60 import java.lang.module.ModuleReference;
  61 
  62 final class JLinkBundlerHelper {
  63 
  64     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  65             "jdk.jpackage.internal.resources.MainResources");
  66     private static final String JRE_MODULES_FILENAME =
  67             "jdk/jpackage/internal/resources/jre.list";
  68     private static final String SERVER_JRE_MODULES_FILENAME =
  69             "jdk/jpackage/internal/resources/jre.module.list";
  70 
  71     static final ToolProvider JLINK_TOOL =
  72             ToolProvider.findFirst("jlink").orElseThrow();
  73 
  74     private JLinkBundlerHelper() {}
  75 
  76     @SuppressWarnings("unchecked")
  77     static final BundlerParamInfo<Integer> DEBUG =
  78             new StandardBundlerParam<>(
  79                     "",
  80                     "",
  81                     "-J-Xdebug",
  82                     Integer.class,
  83                     p -> null,
  84                     (s, p) -> {
  85                         return Integer.valueOf(s);
  86                     });
  87 
  88     static String listOfPathToString(List<Path> value) {
  89         String result = "";
  90 
  91         for (Path path : value) {
  92             if (result.length() > 0) {
  93                 result += File.pathSeparator;
  94             }
  95 
  96             result += path.toString();
  97         }
  98 
  99         return result;
 100     }
 101 
 102     static String setOfStringToString(Set<String> value) {
 103         String result = "";
 104 
 105         for (String element : value) {
 106             if (result.length() > 0) {
 107                 result += ",";
 108             }
 109 
 110             result += element;
 111         }
 112 
 113         return result;
 114     }
 115 
 116     static File getMainJar(Map<String, ? super Object> params) {
 117         File result = null;
 118         RelativeFileSet fileset =
 119                 StandardBundlerParam.MAIN_JAR.fetchFrom(params);
 120 
 121         if (fileset != null) {
 122             String filename = fileset.getIncludedFiles().iterator().next();
 123             result = fileset.getBaseDirectory().toPath().
 124                     resolve(filename).toFile();
 125 
 126             if (result == null || !result.exists()) {
 127                 String srcdir =
 128                     StandardBundlerParam.SOURCE_DIR.fetchFrom(params);
 129 
 130                 if (srcdir != null) {
 131                     result = new File(srcdir + File.separator + filename);
 132                 }
 133             }
 134         }
 135 
 136         return result;
 137     }
 138 
 139     static String getMainClass(Map<String, ? super Object> params) {
 140         String result = "";
 141         String mainModule = StandardBundlerParam.MODULE.fetchFrom(params);
 142         if (mainModule != null)  {
 143             int index = mainModule.indexOf("/");
 144             if (index > 0) {
 145                 result = mainModule.substring(index + 1);
 146             }
 147         } else {
 148             RelativeFileSet fileset =
 149                     StandardBundlerParam.MAIN_JAR.fetchFrom(params);
 150             if (fileset != null) {
 151                 result = StandardBundlerParam.MAIN_CLASS.fetchFrom(params);
 152             } else {
 153                 // possibly app-image
 154             }
 155         }
 156 
 157         return result;
 158     }
 159 
 160     static String getMainModule(Map<String, ? super Object> params) {
 161         String result = null;
 162         String mainModule = StandardBundlerParam.MODULE.fetchFrom(params);
 163 
 164         if (mainModule != null) {
 165             int index = mainModule.indexOf("/");
 166 
 167             if (index > 0) {
 168                 result = mainModule.substring(0, index);
 169             } else {
 170                 result = mainModule;
 171             }
 172         }
 173 
 174         return result;
 175     }
 176 
 177     private static Set<String> getValidModules(List<Path> modulePath,
 178             Set<String> addModules, Set<String> limitModules) {
 179         ModuleHelper moduleHelper = new ModuleHelper(
 180                 modulePath, addModules, limitModules);
 181         return removeInvalidModules(modulePath, moduleHelper.modules());
 182     }
 183 
 184     static void execute(Map<String, ? super Object> params,
 185             AbstractAppImageBuilder imageBuilder)
 186             throws IOException, Exception {
 187         List<Path> modulePath =
 188                 StandardBundlerParam.MODULE_PATH.fetchFrom(params);
 189         Set<String> addModules =
 190                 StandardBundlerParam.ADD_MODULES.fetchFrom(params);
 191         Set<String> limitModules =
 192                 StandardBundlerParam.LIMIT_MODULES.fetchFrom(params);
 193         boolean stripNativeCommands =
 194                 StandardBundlerParam.STRIP_NATIVE_COMMANDS.fetchFrom(params);
 195         Path outputDir = imageBuilder.getRoot();
 196         String excludeFileList = imageBuilder.getExcludeFileList();
 197         File mainJar = getMainJar(params);
 198         ModFile.ModType mainJarType = ModFile.ModType.Unknown;
 199 
 200         if (mainJar != null) {
 201             mainJarType = new ModFile(mainJar).getModType();
 202         } else if (StandardBundlerParam.MODULE.fetchFrom(params) == null) {
 203             // user specified only main class, all jars will be on the classpath
 204             mainJarType = ModFile.ModType.UnnamedJar;
 205         }
 206 
 207         // Modules
 208         String mainModule = getMainModule(params);
 209         if (mainJarType == ModFile.ModType.UnnamedJar) {
 210             // The default for an unnamed jar is ALL_DEFAULT
 211             addModules.add(ModuleHelper.ALL_DEFAULT);
 212         } else if (mainJarType == ModFile.ModType.Unknown ||
 213                 mainJarType == ModFile.ModType.ModularJar) {
 214             if (mainModule == null) {
 215                 addModules.add(ModuleHelper.ALL_DEFAULT);
 216             }
 217         } 
 218 
 219         Set<String> validModules =
 220                   getValidModules(modulePath, addModules, limitModules);
 221         if (mainModule != null) {
 222             validModules.add(mainModule);
 223         }
 224 
 225         Log.verbose(MessageFormat.format(
 226                 I18N.getString("message.modules"), validModules.toString()));
 227 
 228         runJLink(outputDir, modulePath, validModules, limitModules,
 229                 excludeFileList, stripNativeCommands,
 230                 new HashMap<String,String>());
 231 
 232         imageBuilder.prepareApplicationFiles();
 233     }
 234 
 235 
 236     static void generateJre(Map<String, ? super Object> params,
 237             AbstractAppImageBuilder imageBuilder)
 238             throws IOException, Exception {
 239         List<Path> modulePath =
 240                 StandardBundlerParam.MODULE_PATH.fetchFrom(params);
 241         Set<String> addModules =
 242                 StandardBundlerParam.ADD_MODULES.fetchFrom(params);
 243         Set<String> limitModules =
 244                 StandardBundlerParam.LIMIT_MODULES.fetchFrom(params);
 245         boolean stripNativeCommands =
 246                 StandardBundlerParam.STRIP_NATIVE_COMMANDS.fetchFrom(params);
 247         Path outputDir = imageBuilder.getRoot();
 248         addModules.add(ModuleHelper.ALL_MODULE_PATH);
 249         Set<String> redistModules = getValidModules(modulePath,
 250                 addModules, limitModules);
 251         addModules.addAll(redistModules);
 252 
 253         Log.verbose(MessageFormat.format(
 254                 I18N.getString("message.modules"), addModules.toString()));
 255 
 256         runJLink(outputDir, modulePath, addModules, limitModules,
 257                 null, stripNativeCommands, new HashMap<String,String>());
 258 
 259         imageBuilder.prepareJreFiles();
 260     }
 261 
 262     // Returns the path to the JDK modules in the user defined module path.
 263     static Path findPathOfModule( List<Path> modulePath, String moduleName) {
 264 
 265         for (Path path : modulePath) {
 266             Path moduleNamePath = path.resolve(moduleName);
 267 
 268             if (Files.exists(moduleNamePath)) {
 269                 return path;
 270             }
 271         }
 272 
 273         return null;
 274     }
 275 
 276     /*
 277      * Returns the set of modules that would be visible by default for
 278      * a non-modular-aware application consisting of the given elements.
 279      */
 280     private static Set<String> getDefaultModules(
 281             Path[] paths, String[] addModules) {
 282 
 283         // the modules in the run-time image that export an API
 284         Stream<String> systemRoots = ModuleFinder.ofSystem().findAll().stream()
 285                 .map(ModuleReference::descriptor)
 286                 .filter(descriptor -> exportsAPI(descriptor))
 287                 .map(ModuleDescriptor::name);
 288 
 289         Set<String> roots;
 290         if (addModules == null || addModules.length == 0) {
 291             roots = systemRoots.collect(Collectors.toSet());
 292         } else {
 293             var extraRoots =  Stream.of(addModules);
 294             roots = Stream.concat(systemRoots,
 295                     extraRoots).collect(Collectors.toSet());
 296         }
 297 
 298         ModuleFinder finder = ModuleFinder.ofSystem();
 299         if (paths != null && paths.length > 0) {
 300             finder = ModuleFinder.compose(finder, ModuleFinder.of(paths));
 301         }
 302         return Configuration.empty()
 303                 .resolveAndBind(finder, ModuleFinder.of(), roots)
 304                 .modules()
 305                 .stream()
 306                 .map(ResolvedModule::name)
 307                 .collect(Collectors.toSet());
 308     } 
 309 
 310     /*
 311      * Returns true if the given module exports an API to all module.
 312      */
 313     private static boolean exportsAPI(ModuleDescriptor descriptor) {
 314         return descriptor.exports()
 315                 .stream()
 316                 .filter(e -> !e.isQualified())
 317                 .findAny()
 318                 .isPresent();
 319     }
 320 
 321     private static Set<String> removeInvalidModules(
 322             List<Path> modulePath, Set<String> modules) {
 323         Set<String> result = new LinkedHashSet<String>();
 324         ModuleManager mm = new ModuleManager(modulePath);
 325         List<ModFile> lmodfiles =
 326                 mm.getModules(EnumSet.of(ModuleManager.SearchType.ModularJar,
 327                         ModuleManager.SearchType.Jmod,
 328                         ModuleManager.SearchType.ExplodedModule));
 329 
 330         HashMap<String, ModFile> validModules = new HashMap<>();
 331 
 332         for (ModFile modFile : lmodfiles) {
 333             validModules.put(modFile.getModName(), modFile);
 334         }
 335 
 336         for (String name : modules) {
 337             if (validModules.containsKey(name)) {
 338                 result.add(name);
 339             } else {
 340                 Log.error(MessageFormat.format(
 341                         I18N.getString("warning.module.does.not.exist"), name));
 342             }
 343         }
 344 
 345         return result;
 346     }
 347 
 348     private static class ModuleHelper {
 349         // The token for "all modules on the module path".
 350         private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
 351 
 352         // The token for "all valid runtime modules".
 353         static final String ALL_DEFAULT = "ALL-DEFAULT";
 354 
 355         private final Set<String> modules = new HashSet<>();
 356         private enum Macros {None, AllModulePath, AllRuntime}
 357 
 358         ModuleHelper(List<Path> paths, Set<String> addModules,
 359                 Set<String> limitModules) {
 360             boolean addAllModulePath = false;
 361             boolean addDefaultMods = false;
 362             
 363             for (Iterator<String> iterator = addModules.iterator();
 364                     iterator.hasNext();) {
 365                 String module = iterator.next();
 366 
 367                 switch (module) {
 368                     case ALL_MODULE_PATH:
 369                         iterator.remove();
 370                         addAllModulePath = true;
 371                         break;
 372                     case ALL_DEFAULT:
 373                         iterator.remove();
 374                         addDefaultMods = true;
 375                         break;
 376                     default:
 377                         this.modules.add(module);
 378                 }
 379             }
 380 
 381             if (addAllModulePath) {
 382                 this.modules.addAll(getModuleNamesFromPath(paths));
 383             } else if (addDefaultMods) {
 384                 this.modules.addAll(getDefaultModules(
 385                         paths.toArray(new Path[0]),
 386                         addModules.toArray(new String[0])));
 387             }
 388         }
 389 
 390         Set<String> modules() {
 391             return modules;
 392         }
 393 
 394         private static Set<String> getModuleNamesFromPath(List<Path> Value) {
 395             Set<String> result = new LinkedHashSet<String>();
 396             ModuleManager mm = new ModuleManager(Value);
 397             List<ModFile> modFiles = mm.getModules(
 398                     EnumSet.of(ModuleManager.SearchType.ModularJar,
 399                     ModuleManager.SearchType.Jmod,
 400                     ModuleManager.SearchType.ExplodedModule));
 401 
 402             for (ModFile modFile : modFiles) {
 403                 result.add(modFile.getModName());
 404             }
 405             return result;
 406         }
 407     }
 408 
 409     private static void runJLink(Path output, List<Path> modulePath,
 410             Set<String> modules, Set<String> limitModules, String excludes,
 411             boolean strip, HashMap<String, String> user) throws IOException {
 412 
 413         // This is just to ensure jlink is given a non-existant directory
 414         // The passed in output path should be non-existant or empty directory
 415         IOUtils.deleteRecursive(output.toFile());
 416 
 417         ArrayList<String> args = new ArrayList<String>();
 418         args.add("--output");
 419         args.add(output.toString());
 420         if (modulePath != null && !modulePath.isEmpty()) {
 421             args.add("--module-path");
 422             args.add(getPathList(modulePath));
 423         }
 424         if (modules != null && !modules.isEmpty()) {
 425             args.add("--add-modules");
 426             args.add(getStringList(modules));
 427         }
 428         if (limitModules != null && !limitModules.isEmpty()) {
 429             args.add("--limit-modules");
 430             args.add(getStringList(limitModules));
 431         }
 432         if (excludes != null) {
 433             args.add("--exclude-files");
 434             args.add(excludes);
 435         }
 436         if (strip) {
 437             args.add("--strip-native-commands");
 438         }
 439         for (Map.Entry<String, String> entry : user.entrySet()) {
 440             args.add(entry.getKey());
 441             args.add(entry.getValue());
 442         }
 443         args.add("--strip-debug");
 444         args.add("--no-header-files");
 445         args.add("--bind-services");
 446         
 447         StringWriter writer = new StringWriter();
 448         PrintWriter pw = new PrintWriter(writer);
 449 
 450         Log.verbose("jlink arguments: " + args);
 451         int retVal = JLINK_TOOL.run(pw, pw, args.toArray(new String[0]));
 452         String jlinkOut = writer.toString();
 453 
 454         if (retVal != 0) {
 455             throw new IOException("jlink failed with: " + jlinkOut);
 456         } else if (jlinkOut.length() > 0) {
 457             Log.verbose("jlink output: " + jlinkOut);
 458         }
 459     }
 460 
 461     private static String getPathList(List<Path> pathList) {
 462         String ret = null;
 463         for (Path p : pathList) {
 464             String s =  Matcher.quoteReplacement(p.toString());
 465             if (ret == null) {
 466                 ret = s;
 467             } else {
 468                 ret += File.pathSeparator +  s;
 469             }
 470         }
 471         return ret;
 472     }
 473 
 474     private static String getStringList(Set<String> strings) {
 475         String ret = null;
 476         for (String s : strings) {
 477             if (ret == null) {
 478                 ret = s;
 479             } else {
 480                 ret += "," + s;
 481             }
 482         }
 483         return (ret == null) ? null : Matcher.quoteReplacement(ret);
 484     }
 485 }