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