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 }