1 /* 2 * Copyright (c) 2015, 2016, 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 package jdk.tools.jlink.internal; 26 27 import java.io.File; 28 import java.io.IOException; 29 import java.io.PrintWriter; 30 import java.io.UncheckedIOException; 31 import java.lang.module.Configuration; 32 import java.lang.module.ModuleFinder; 33 import java.lang.module.ModuleReference; 34 import java.lang.module.ResolutionException; 35 import java.lang.module.ResolvedModule; 36 import java.lang.reflect.InvocationTargetException; 37 import java.net.URI; 38 import java.nio.ByteOrder; 39 import java.nio.file.Files; 40 import java.nio.file.Path; 41 import java.nio.file.Paths; 42 import java.util.Date; 43 import java.util.Formatter; 44 import java.util.HashMap; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Objects; 49 import java.util.Optional; 50 import java.util.Set; 51 import java.util.stream.Collectors; 52 53 import jdk.internal.module.ConfigurableModuleFinder; 54 import jdk.internal.module.ConfigurableModuleFinder.Phase; 55 import jdk.tools.jlink.internal.TaskHelper.BadArgs; 56 import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE; 57 import jdk.tools.jlink.internal.TaskHelper.Option; 58 import jdk.tools.jlink.internal.TaskHelper.OptionsHelper; 59 import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider; 60 import jdk.tools.jlink.plugin.ExecutableImage; 61 import jdk.tools.jlink.Jlink.JlinkConfiguration; 62 import jdk.tools.jlink.Jlink.PluginsConfiguration; 63 import jdk.tools.jlink.plugin.PluginException; 64 import jdk.tools.jlink.builder.DefaultImageBuilder; 65 import jdk.tools.jlink.plugin.Plugin; 66 67 /** 68 * Implementation for the jlink tool. 69 * 70 * ## Should use jdk.joptsimple some day. 71 */ 72 public class JlinkTask { 73 74 private static <T extends Throwable> void fail(Class<T> type, 75 String format, 76 Object... args) throws T { 77 String msg = new Formatter().format(format, args).toString(); 78 try { 79 T t = type.getConstructor(String.class).newInstance(msg); 80 throw t; 81 } catch (InstantiationException | 82 InvocationTargetException | 83 NoSuchMethodException | 84 IllegalAccessException e) { 85 throw new InternalError("Unable to create an instance of " + type, e); 86 } 87 } 88 89 private static final TaskHelper taskHelper 90 = new TaskHelper(JLINK_BUNDLE); 91 92 static Option<?>[] recognizedOptions = { 93 new Option<JlinkTask>(false, (task, opt, arg) -> { 94 task.options.help = true; 95 }, "--help"), 96 new Option<JlinkTask>(true, (task, opt, arg) -> { 97 String[] dirs = arg.split(File.pathSeparator); 98 task.options.modulePath = new Path[dirs.length]; 99 int i = 0; 100 for (String dir : dirs) { 101 task.options.modulePath[i++] = Paths.get(dir); 102 } 103 }, "--modulepath", "--mp"), 104 new Option<JlinkTask>(true, (task, opt, arg) -> { 105 for (String mn : arg.split(",")) { 106 if (mn.isEmpty()) { 107 throw taskHelper.newBadArgs("err.mods.must.be.specified", 108 "--limitmods"); 109 } 110 task.options.limitMods.add(mn); 111 } 112 }, "--limitmods"), 113 new Option<JlinkTask>(true, (task, opt, arg) -> { 114 for (String mn : arg.split(",")) { 115 if (mn.isEmpty()) { 116 throw taskHelper.newBadArgs("err.mods.must.be.specified", 117 "--addmods"); 118 } 119 task.options.addMods.add(mn); 120 } 121 }, "--addmods"), 122 new Option<JlinkTask>(true, (task, opt, arg) -> { 123 Path path = Paths.get(arg); 124 task.options.output = path; 125 }, "--output"), 126 new Option<JlinkTask>(true, (task, opt, arg) -> { 127 if ("little".equals(arg)) { 128 task.options.endian = ByteOrder.LITTLE_ENDIAN; 129 } else if ("big".equals(arg)) { 130 task.options.endian = ByteOrder.BIG_ENDIAN; 131 } else { 132 throw taskHelper.newBadArgs("err.unknown.byte.order", arg); 133 } 134 }, "--endian"), 135 new Option<JlinkTask>(false, (task, opt, arg) -> { 136 task.options.version = true; 137 }, "--version"), 138 new Option<JlinkTask>(true, (task, opt, arg) -> { 139 Path path = Paths.get(arg); 140 if (Files.exists(path)) { 141 throw taskHelper.newBadArgs("err.dir.exists", path); 142 } 143 task.options.packagedModulesPath = path; 144 }, true, "--keep-packaged-modules"), 145 new Option<JlinkTask>(false, (task, opt, arg) -> { 146 task.options.genbom = true; 147 }, true, "--genbom"), 148 new Option<JlinkTask>(true, (task, opt, arg) -> { 149 task.options.saveoptsfile = arg; 150 }, "--saveopts"), 151 new Option<JlinkTask>(false, (task, opt, arg) -> { 152 task.options.fullVersion = true; 153 }, true, "--fullversion"),}; 154 155 private static final String PROGNAME = "jlink"; 156 private final OptionsValues options = new OptionsValues(); 157 158 private static final OptionsHelper<JlinkTask> optionsHelper 159 = taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions); 160 private PrintWriter log; 161 162 void setLog(PrintWriter out) { 163 log = out; 164 taskHelper.setLog(log); 165 } 166 167 /** 168 * Result codes. 169 */ 170 static final int EXIT_OK = 0, // Completed with no errors. 171 EXIT_ERROR = 1, // Completed but reported errors. 172 EXIT_CMDERR = 2, // Bad command-line arguments 173 EXIT_SYSERR = 3, // System error or resource exhaustion. 174 EXIT_ABNORMAL = 4;// terminated abnormally 175 176 static class OptionsValues { 177 boolean help; 178 boolean genbom; 179 String saveoptsfile; 180 boolean version; 181 boolean fullVersion; 182 Path[] modulePath; 183 Set<String> limitMods = new HashSet<>(); 184 Set<String> addMods = new HashSet<>(); 185 Path output; 186 Path packagedModulesPath; 187 ByteOrder endian = ByteOrder.nativeOrder(); 188 } 189 190 int run(String[] args) { 191 if (log == null) { 192 setLog(new PrintWriter(System.err)); 193 } 194 try { 195 optionsHelper.handleOptions(this, args); 196 if (options.help) { 197 optionsHelper.showHelp(PROGNAME); 198 return EXIT_OK; 199 } 200 if (optionsHelper.listPlugins()) { 201 optionsHelper.listPlugins(true); 202 return EXIT_OK; 203 } 204 if (options.version || options.fullVersion) { 205 taskHelper.showVersion(options.fullVersion); 206 return EXIT_OK; 207 } 208 if (taskHelper.getExistingImage() == null) { 209 if (options.modulePath == null || options.modulePath.length == 0) { 210 throw taskHelper.newBadArgs("err.modulepath.must.be.specified").showUsage(true); 211 } 212 createImage(); 213 } else { 214 postProcessOnly(taskHelper.getExistingImage()); 215 } 216 217 if (options.saveoptsfile != null) { 218 Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes()); 219 } 220 221 return EXIT_OK; 222 } catch (UncheckedIOException | PluginException | IOException | ResolutionException e) { 223 log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage()); 224 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME)); 225 return EXIT_ERROR; 226 } catch (BadArgs e) { 227 taskHelper.reportError(e.key, e.args); 228 if (e.showUsage) { 229 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME)); 230 } 231 return EXIT_CMDERR; 232 } catch (Throwable x) { 233 log.println(taskHelper.getMessage("main.msg.bug")); 234 x.printStackTrace(log); 235 return EXIT_ABNORMAL; 236 } finally { 237 log.flush(); 238 } 239 } 240 241 private static Map<String, Path> modulesToPath(Configuration cf) { 242 Map<String, Path> modPaths = new HashMap<>(); 243 for (ResolvedModule resolvedModule : cf.modules()) { 244 ModuleReference mref = resolvedModule.reference(); 245 URI uri = mref.location().getWhenPresent(); 246 modPaths.put(mref.descriptor().name(), Paths.get(uri)); 247 } 248 return modPaths; 249 } 250 251 /* 252 * Jlink API entry point. 253 */ 254 public static void createImage(JlinkConfiguration config, 255 PluginsConfiguration plugins) 256 throws Exception { 257 Objects.requireNonNull(config); 258 Objects.requireNonNull(config.getOutput()); 259 plugins = plugins == null ? new PluginsConfiguration() : plugins; 260 261 if (config.getModulepaths().isEmpty()) { 262 throw new Exception("Empty module paths"); 263 } 264 Path[] arr = new Path[config.getModulepaths().size()]; 265 arr = config.getModulepaths().toArray(arr); 266 ModuleFinder finder 267 = newModuleFinder(arr, config.getLimitmods(), config.getModules()); 268 269 // First create the image provider 270 ImageProvider imageProvider 271 = createImageProvider(finder, 272 checkAddMods(config.getModules()), 273 config.getLimitmods(), 274 config.getByteOrder(), 275 null); 276 277 // Then create the Plugin Stack 278 ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins, 279 genBOMContent(config, plugins)); 280 281 //Ask the stack to proceed; 282 stack.operate(imageProvider); 283 } 284 285 /* 286 * Jlink API entry point. 287 */ 288 public static void postProcessImage(ExecutableImage image, List<Plugin> postProcessorPlugins) 289 throws Exception { 290 Objects.requireNonNull(image); 291 Objects.requireNonNull(postProcessorPlugins); 292 PluginsConfiguration config = new PluginsConfiguration(postProcessorPlugins); 293 ImagePluginStack stack = ImagePluginConfiguration. 294 parseConfiguration(config); 295 296 stack.operate((ImagePluginStack stack1) -> image); 297 } 298 299 private void postProcessOnly(Path existingImage) throws Exception { 300 PluginsConfiguration config = taskHelper.getPluginsConfig(null, false); 301 ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage); 302 if (img == null) { 303 throw taskHelper.newBadArgs("err.existing.image.invalid"); 304 } 305 postProcessImage(img, config.getPlugins()); 306 } 307 308 private void createImage() throws Exception { 309 if (options.output == null) { 310 throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true); 311 } 312 ModuleFinder finder 313 = newModuleFinder(options.modulePath, options.limitMods, options.addMods); 314 try { 315 options.addMods = checkAddMods(options.addMods); 316 } catch (IllegalArgumentException ex) { 317 throw taskHelper.newBadArgs("err.mods.must.be.specified", "--addmods") 318 .showUsage(true); 319 } 320 // First create the image provider 321 ImageProvider imageProvider 322 = createImageProvider(finder, 323 options.addMods, 324 options.limitMods, 325 options.endian, 326 options.packagedModulesPath); 327 328 // Then create the Plugin Stack 329 ImagePluginStack stack = ImagePluginConfiguration. 330 parseConfiguration(taskHelper.getPluginsConfig(options.output, options.genbom), 331 genBOMContent()); 332 333 //Ask the stack to proceed 334 stack.operate(imageProvider); 335 } 336 337 private static Set<String> checkAddMods(Set<String> addMods) { 338 if (addMods.isEmpty()) { 339 throw new IllegalArgumentException("no modules to add"); 340 } 341 return addMods; 342 } 343 344 private static ModuleFinder newModuleFinder(Path[] paths, 345 Set<String> limitMods, 346 Set<String> addMods) { 347 ModuleFinder finder = ModuleFinder.of(paths); 348 349 // jmods are located at link-time 350 if (finder instanceof ConfigurableModuleFinder) { 351 ((ConfigurableModuleFinder) finder).configurePhase(Phase.LINK_TIME); 352 } 353 354 // if limitmods is specified then limit the universe 355 if (!limitMods.isEmpty()) { 356 finder = limitFinder(finder, limitMods, addMods); 357 } 358 return finder; 359 } 360 361 private static ImageProvider createImageProvider(ModuleFinder finder, 362 Set<String> addMods, 363 Set<String> limitMods, 364 ByteOrder order, 365 Path retainModulesPath) 366 throws IOException 367 { 368 if (addMods.isEmpty()) { 369 throw new IllegalArgumentException("empty modules and limitmods"); 370 } 371 372 Configuration cf = Configuration.empty() 373 .resolveRequires(finder, 374 ModuleFinder.empty(), 375 addMods); 376 377 Map<String, Path> mods = modulesToPath(cf); 378 return new ImageHelper(cf, mods, order, retainModulesPath); 379 } 380 381 /** 382 * Returns a ModuleFinder that limits observability to the given root 383 * modules, their transitive dependences, plus a set of other modules. 384 */ 385 private static ModuleFinder limitFinder(ModuleFinder finder, 386 Set<String> roots, 387 Set<String> otherMods) { 388 389 // resolve all root modules 390 Configuration cf = Configuration.empty() 391 .resolveRequires(finder, 392 ModuleFinder.empty(), 393 roots); 394 395 // module name -> reference 396 Map<String, ModuleReference> map = new HashMap<>(); 397 cf.modules().forEach(m -> { 398 ModuleReference mref = m.reference(); 399 map.put(mref.descriptor().name(), mref); 400 }); 401 402 // set of modules that are observable 403 Set<ModuleReference> mrefs = new HashSet<>(map.values()); 404 405 // add the other modules 406 for (String mod : otherMods) { 407 Optional<ModuleReference> omref = finder.find(mod); 408 if (omref.isPresent()) { 409 ModuleReference mref = omref.getWhenPresent(); 410 map.putIfAbsent(mod, mref); 411 mrefs.add(mref); 412 } else { 413 // no need to fail 414 } 415 } 416 417 return new ModuleFinder() { 418 @Override 419 public Optional<ModuleReference> find(String name) { 420 return Optional.ofNullable(map.get(name)); 421 } 422 423 @Override 424 public Set<ModuleReference> findAll() { 425 return mrefs; 426 } 427 }; 428 } 429 430 private String getSaveOpts() { 431 StringBuilder sb = new StringBuilder(); 432 sb.append('#').append(new Date()).append("\n"); 433 for (String c : optionsHelper.getInputCommand()) { 434 sb.append(c).append(" "); 435 } 436 437 return sb.toString(); 438 } 439 440 private static String getBomHeader() { 441 StringBuilder sb = new StringBuilder(); 442 sb.append("#").append(new Date()).append("\n"); 443 sb.append("#Please DO NOT Modify this file").append("\n"); 444 return sb.toString(); 445 } 446 447 private String genBOMContent() throws IOException { 448 StringBuilder sb = new StringBuilder(); 449 sb.append(getBomHeader()); 450 StringBuilder command = new StringBuilder(); 451 for (String c : optionsHelper.getInputCommand()) { 452 command.append(c).append(" "); 453 } 454 sb.append("command").append(" = ").append(command); 455 sb.append("\n"); 456 457 return sb.toString(); 458 } 459 460 private static String genBOMContent(JlinkConfiguration config, 461 PluginsConfiguration plugins) 462 throws IOException { 463 StringBuilder sb = new StringBuilder(); 464 sb.append(getBomHeader()); 465 sb.append(config); 466 sb.append(plugins); 467 return sb.toString(); 468 } 469 470 private static class ImageHelper implements ImageProvider { 471 472 final Set<Archive> archives; 473 final ByteOrder order; 474 final Path packagedModulesPath; 475 476 ImageHelper(Configuration cf, 477 Map<String, Path> modsPaths, 478 ByteOrder order, 479 Path packagedModulesPath) throws IOException { 480 archives = modsPaths.entrySet().stream() 481 .map(e -> newArchive(e.getKey(), e.getValue())) 482 .collect(Collectors.toSet()); 483 this.order = order; 484 this.packagedModulesPath = packagedModulesPath; 485 } 486 487 private Archive newArchive(String module, Path path) { 488 if (path.toString().endsWith(".jmod")) { 489 return new JmodArchive(module, path); 490 } else if (path.toString().endsWith(".jar")) { 491 return new ModularJarArchive(module, path); 492 } else if (Files.isDirectory(path)) { 493 return new DirArchive(path); 494 } else { 495 fail(RuntimeException.class, 496 "Selected module %s (%s) not in jmod or modular jar format", 497 module, 498 path); 499 } 500 return null; 501 } 502 503 @Override 504 public ExecutableImage retrieve(ImagePluginStack stack) throws IOException { 505 ExecutableImage image = ImageFileCreator.create(archives, order, stack); 506 if (packagedModulesPath != null) { 507 // copy the packaged modules to the given path 508 Files.createDirectories(packagedModulesPath); 509 for (Archive a : archives) { 510 Path file = a.getPath(); 511 Path dest = packagedModulesPath.resolve(file.getFileName()); 512 Files.copy(file, dest); 513 } 514 } 515 return image; 516 } 517 } 518 519 private static enum Section { 520 NATIVE_LIBS("native", nativeDir()), 521 NATIVE_CMDS("bin", "bin"), 522 CLASSES("classes", "classes"), 523 CONFIG("conf", "conf"), 524 UNKNOWN("unknown", "unknown"); 525 526 private static String nativeDir() { 527 if (System.getProperty("os.name").startsWith("Windows")) { 528 return "bin"; 529 } else { 530 return "lib"; 531 } 532 } 533 534 private final String jmodDir; 535 private final String imageDir; 536 537 Section(String jmodDir, String imageDir) { 538 this.jmodDir = jmodDir; 539 this.imageDir = imageDir; 540 } 541 542 String imageDir() { 543 return imageDir; 544 } 545 546 String jmodDir() { 547 return jmodDir; 548 } 549 550 boolean matches(String path) { 551 return path.startsWith(jmodDir); 552 } 553 554 static Section getSectionFromName(String dir) { 555 if (Section.NATIVE_LIBS.matches(dir)) { 556 return Section.NATIVE_LIBS; 557 } else if (Section.NATIVE_CMDS.matches(dir)) { 558 return Section.NATIVE_CMDS; 559 } else if (Section.CLASSES.matches(dir)) { 560 return Section.CLASSES; 561 } else if (Section.CONFIG.matches(dir)) { 562 return Section.CONFIG; 563 } else { 564 return Section.UNKNOWN; 565 } 566 } 567 } 568 }