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