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 
  67     static final ToolProvider JLINK_TOOL =
  68             ToolProvider.findFirst("jlink").orElseThrow();
  69 
  70     static File getMainJar(Map<String, ? super Object> params) {
  71         File result = null;
  72         RelativeFileSet fileset =
  73                 StandardBundlerParam.MAIN_JAR.fetchFrom(params);
  74 
  75         if (fileset != null) {
  76             String filename = fileset.getIncludedFiles().iterator().next();
  77             result = fileset.getBaseDirectory().toPath().
  78                     resolve(filename).toFile();
  79 
  80             if (result == null || !result.exists()) {
  81                 String srcdir =
  82                     StandardBundlerParam.SOURCE_DIR.fetchFrom(params);
  83 
  84                 if (srcdir != null) {
  85                     result = new File(srcdir + File.separator + filename);
  86                 }
  87             }
  88         }
  89 
  90         return result;
  91     }
  92 
  93     static String getMainClass(Map<String, ? super Object> params) {
  94         String result = "";
  95         String mainModule = StandardBundlerParam.MODULE.fetchFrom(params);
  96         if (mainModule != null)  {
  97             int index = mainModule.indexOf("/");
  98             if (index > 0) {
  99                 result = mainModule.substring(index + 1);
 100             }
 101         } else {
 102             RelativeFileSet fileset =
 103                     StandardBundlerParam.MAIN_JAR.fetchFrom(params);
 104             if (fileset != null) {
 105                 result = StandardBundlerParam.MAIN_CLASS.fetchFrom(params);
 106             } else {
 107                 // possibly app-image
 108             }
 109         }
 110 
 111         return result;
 112     }
 113 
 114     static String getMainModule(Map<String, ? super Object> params) {
 115         String result = null;
 116         String mainModule = StandardBundlerParam.MODULE.fetchFrom(params);
 117 
 118         if (mainModule != null) {
 119             int index = mainModule.indexOf("/");
 120 
 121             if (index > 0) {
 122                 result = mainModule.substring(0, index);
 123             } else {
 124                 result = mainModule;
 125             }
 126         }
 127 
 128         return result;
 129     }
 130 
 131     private static Set<String> getValidModules(List<Path> modulePath,
 132             Set<String> addModules, Set<String> limitModules) {
 133         ModuleHelper moduleHelper = new ModuleHelper(
 134                 modulePath, addModules, limitModules);
 135         return removeInvalidModules(modulePath, moduleHelper.modules());
 136     }
 137 
 138     static void execute(Map<String, ? super Object> params,
 139             AbstractAppImageBuilder imageBuilder)
 140             throws IOException, Exception {
 141 
 142         // we might be able to build it (with no main class) but it won't run
 143         if (StandardBundlerParam.MAIN_CLASS.fetchFrom(params) == null) {
 144             throw new PackagerException("ERR_NoMainClass");
 145         }
 146 
 147         List<Path> modulePath =
 148                 StandardBundlerParam.MODULE_PATH.fetchFrom(params);
 149         Set<String> addModules =
 150                 StandardBundlerParam.ADD_MODULES.fetchFrom(params);
 151         Set<String> limitModules =
 152                 StandardBundlerParam.LIMIT_MODULES.fetchFrom(params);
 153         Path outputDir = imageBuilder.getRoot();
 154         File mainJar = getMainJar(params);
 155         ModFile.ModType mainJarType = ModFile.ModType.Unknown;
 156 
 157         if (mainJar != null) {
 158             mainJarType = new ModFile(mainJar).getModType();
 159         } else if (StandardBundlerParam.MODULE.fetchFrom(params) == null) {
 160             // user specified only main class, all jars will be on the classpath
 161             mainJarType = ModFile.ModType.UnnamedJar;
 162         }
 163 
 164         boolean bindServices = addModules.isEmpty();
 165 
 166         // Modules
 167         String mainModule = getMainModule(params);
 168         if (mainModule == null) {
 169             if (mainJarType == ModFile.ModType.UnnamedJar) {
 170                 if (addModules.isEmpty()) {
 171                     // The default for an unnamed jar is ALL_DEFAULT
 172                     addModules.add(ModuleHelper.ALL_DEFAULT);
 173                 }
 174             } else if (mainJarType == ModFile.ModType.Unknown ||
 175                     mainJarType == ModFile.ModType.ModularJar) {
 176                 addModules.add(ModuleHelper.ALL_DEFAULT);
 177             }
 178         }
 179 
 180         Set<String> validModules =
 181                   getValidModules(modulePath, addModules, limitModules);
 182 
 183         if (mainModule != null) {
 184             validModules.add(mainModule);
 185         }
 186 
 187         runJLink(outputDir, modulePath, validModules, limitModules,
 188                 new HashMap<String,String>(), bindServices);
 189 
 190         imageBuilder.prepareApplicationFiles();
 191     }
 192 
 193 
 194     // Returns the path to the JDK modules in the user defined module path.
 195     static Path findPathOfModule( List<Path> modulePath, String moduleName) {
 196 
 197         for (Path path : modulePath) {
 198             Path moduleNamePath = path.resolve(moduleName);
 199 
 200             if (Files.exists(moduleNamePath)) {
 201                 return path;
 202             }
 203         }
 204 
 205         return null;
 206     }
 207 
 208     /*
 209      * Returns the set of modules that would be visible by default for
 210      * a non-modular-aware application consisting of the given elements.
 211      */
 212     private static Set<String> getDefaultModules(
 213             Path[] paths, String[] addModules) {
 214 
 215         // the modules in the run-time image that export an API
 216         Stream<String> systemRoots = ModuleFinder.ofSystem().findAll().stream()
 217                 .map(ModuleReference::descriptor)
 218                 .filter(descriptor -> exportsAPI(descriptor))
 219                 .map(ModuleDescriptor::name);
 220 
 221         Set<String> roots;
 222         if (addModules == null || addModules.length == 0) {
 223             roots = systemRoots.collect(Collectors.toSet());
 224         } else {
 225             var extraRoots =  Stream.of(addModules);
 226             roots = Stream.concat(systemRoots,
 227                     extraRoots).collect(Collectors.toSet());
 228         }
 229 
 230         ModuleFinder finder = ModuleFinder.ofSystem();
 231         if (paths != null && paths.length > 0) {
 232             finder = ModuleFinder.compose(finder, ModuleFinder.of(paths));
 233         }
 234         return Configuration.empty()
 235                 .resolveAndBind(finder, ModuleFinder.of(), roots)
 236                 .modules()
 237                 .stream()
 238                 .map(ResolvedModule::name)
 239                 .collect(Collectors.toSet());
 240     }
 241 
 242     /*
 243      * Returns true if the given module exports an API to all module.
 244      */
 245     private static boolean exportsAPI(ModuleDescriptor descriptor) {
 246         return descriptor.exports()
 247                 .stream()
 248                 .filter(e -> !e.isQualified())
 249                 .findAny()
 250                 .isPresent();
 251     }
 252 
 253     private static Set<String> removeInvalidModules(
 254             List<Path> modulePath, Set<String> modules) {
 255         Set<String> result = new LinkedHashSet<String>();
 256         ModuleManager mm = new ModuleManager(modulePath);
 257         List<ModFile> lmodfiles =
 258                 mm.getModules(EnumSet.of(ModuleManager.SearchType.ModularJar,
 259                         ModuleManager.SearchType.Jmod,
 260                         ModuleManager.SearchType.ExplodedModule));
 261 
 262         HashMap<String, ModFile> validModules = new HashMap<>();
 263 
 264         for (ModFile modFile : lmodfiles) {
 265             validModules.put(modFile.getModName(), modFile);
 266         }
 267 
 268         for (String name : modules) {
 269             if (validModules.containsKey(name)) {
 270                 result.add(name);
 271             } else {
 272                 Log.error(MessageFormat.format(
 273                         I18N.getString("warning.module.does.not.exist"), name));
 274             }
 275         }
 276 
 277         return result;
 278     }
 279 
 280     private static class ModuleHelper {
 281         // The token for "all modules on the module path".
 282         private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH";
 283 
 284         // The token for "all valid runtime modules".
 285         static final String ALL_DEFAULT = "ALL-DEFAULT";
 286 
 287         private final Set<String> modules = new HashSet<>();
 288         private enum Macros {None, AllModulePath, AllRuntime}
 289 
 290         ModuleHelper(List<Path> paths, Set<String> addModules,
 291                 Set<String> limitModules) {
 292             boolean addAllModulePath = false;
 293             boolean addDefaultMods = false;
 294 
 295             for (Iterator<String> iterator = addModules.iterator();
 296                     iterator.hasNext();) {
 297                 String module = iterator.next();
 298 
 299                 switch (module) {
 300                     case ALL_MODULE_PATH:
 301                         iterator.remove();
 302                         addAllModulePath = true;
 303                         break;
 304                     case ALL_DEFAULT:
 305                         iterator.remove();
 306                         addDefaultMods = true;
 307                         break;
 308                     default:
 309                         this.modules.add(module);
 310                 }
 311             }
 312 
 313             if (addAllModulePath) {
 314                 this.modules.addAll(getModuleNamesFromPath(paths));
 315             } else if (addDefaultMods) {
 316                 this.modules.addAll(getDefaultModules(
 317                         paths.toArray(new Path[0]),
 318                         addModules.toArray(new String[0])));
 319             }
 320         }
 321 
 322         Set<String> modules() {
 323             return modules;
 324         }
 325 
 326         private static Set<String> getModuleNamesFromPath(List<Path> Value) {
 327             Set<String> result = new LinkedHashSet<String>();
 328             ModuleManager mm = new ModuleManager(Value);
 329             List<ModFile> modFiles = mm.getModules(
 330                     EnumSet.of(ModuleManager.SearchType.ModularJar,
 331                     ModuleManager.SearchType.Jmod,
 332                     ModuleManager.SearchType.ExplodedModule));
 333 
 334             for (ModFile modFile : modFiles) {
 335                 result.add(modFile.getModName());
 336             }
 337             return result;
 338         }
 339     }
 340 
 341     private static void runJLink(Path output, List<Path> modulePath,
 342             Set<String> modules, Set<String> limitModules,
 343             HashMap<String, String> user, boolean bindServices)
 344             throws IOException {
 345 
 346         // This is just to ensure jlink is given a non-existant directory
 347         // The passed in output path should be non-existant or empty directory
 348         IOUtils.deleteRecursive(output.toFile());
 349 
 350         ArrayList<String> args = new ArrayList<String>();
 351         args.add("--output");
 352         args.add(output.toString());
 353         if (modulePath != null && !modulePath.isEmpty()) {
 354             args.add("--module-path");
 355             args.add(getPathList(modulePath));
 356         }
 357         if (modules != null && !modules.isEmpty()) {
 358             args.add("--add-modules");
 359             args.add(getStringList(modules));
 360         }
 361         if (limitModules != null && !limitModules.isEmpty()) {
 362             args.add("--limit-modules");
 363             args.add(getStringList(limitModules));
 364         }
 365         if (user != null && !user.isEmpty()) {
 366             for (Map.Entry<String, String> entry : user.entrySet()) {
 367                 args.add(entry.getKey());
 368                 args.add(entry.getValue());
 369             }
 370         } else {
 371             args.add("--strip-native-commands");
 372             args.add("--strip-debug");
 373             args.add("--no-man-pages");
 374             args.add("--no-header-files");
 375             if (bindServices) {
 376                 args.add("--bind-services");
 377             }
 378         }
 379 
 380         StringWriter writer = new StringWriter();
 381         PrintWriter pw = new PrintWriter(writer);
 382 
 383         Log.verbose("jlink arguments: " + args);
 384         int retVal = JLINK_TOOL.run(pw, pw, args.toArray(new String[0]));
 385         String jlinkOut = writer.toString();
 386 
 387         if (retVal != 0) {
 388             throw new IOException("jlink failed with: " + jlinkOut);
 389         } else if (jlinkOut.length() > 0) {
 390             Log.verbose("jlink output: " + jlinkOut);
 391         }
 392     }
 393 
 394     private static String getPathList(List<Path> pathList) {
 395         String ret = null;
 396         for (Path p : pathList) {
 397             String s =  Matcher.quoteReplacement(p.toString());
 398             if (ret == null) {
 399                 ret = s;
 400             } else {
 401                 ret += File.pathSeparator +  s;
 402             }
 403         }
 404         return ret;
 405     }
 406 
 407     private static String getStringList(Set<String> strings) {
 408         String ret = null;
 409         for (String s : strings) {
 410             if (ret == null) {
 411                 ret = s;
 412             } else {
 413                 ret += "," + s;
 414             }
 415         }
 416         return (ret == null) ? null : Matcher.quoteReplacement(ret);
 417     }
 418 }