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 }