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.FindException; 33 import java.lang.module.ModuleDescriptor; 34 import java.lang.module.ModuleFinder; 35 import java.lang.module.ModuleReference; 36 import java.lang.module.ResolutionException; 37 import java.lang.module.ResolvedModule; 38 import java.net.URI; 39 import java.nio.ByteOrder; 40 import java.nio.file.Files; 41 import java.nio.file.Path; 42 import java.nio.file.Paths; 43 import java.util.*; 44 import java.util.stream.Collectors; 45 import java.util.stream.Stream; 46 47 import jdk.tools.jlink.internal.TaskHelper.BadArgs; 48 import static jdk.tools.jlink.internal.TaskHelper.JLINK_BUNDLE; 49 import jdk.tools.jlink.internal.Jlink.JlinkConfiguration; 50 import jdk.tools.jlink.internal.Jlink.PluginsConfiguration; 51 import jdk.tools.jlink.internal.TaskHelper.Option; 52 import jdk.tools.jlink.internal.TaskHelper.OptionsHelper; 53 import jdk.tools.jlink.internal.ImagePluginStack.ImageProvider; 54 import jdk.tools.jlink.plugin.PluginException; 55 import jdk.tools.jlink.builder.DefaultImageBuilder; 56 import jdk.tools.jlink.plugin.Plugin; 57 import jdk.internal.module.Checks; 58 import jdk.internal.module.ModulePath; 59 import jdk.internal.module.ModuleResolution; 60 61 /** 62 * Implementation for the jlink tool. 63 * 64 * ## Should use jdk.joptsimple some day. 65 */ 66 public class JlinkTask { 67 static final boolean DEBUG = Boolean.getBoolean("jlink.debug"); 68 69 // jlink API ignores by default. Remove when signing is implemented. 70 static final boolean IGNORE_SIGNING_DEFAULT = true; 71 72 private static final TaskHelper taskHelper 73 = new TaskHelper(JLINK_BUNDLE); 74 75 private static final Option<?>[] recognizedOptions = { 76 new Option<JlinkTask>(false, (task, opt, arg) -> { 77 task.options.help = true; 78 }, "--help", "-h"), 79 new Option<JlinkTask>(true, (task, opt, arg) -> { 80 // if used multiple times, the last one wins! 81 // So, clear previous values, if any. 82 task.options.modulePath.clear(); 83 String[] dirs = arg.split(File.pathSeparator); 84 int i = 0; 85 Arrays.stream(dirs) 86 .map(Paths::get) 87 .forEach(task.options.modulePath::add); 88 }, "--module-path", "-p"), 89 new Option<JlinkTask>(true, (task, opt, arg) -> { 90 // if used multiple times, the last one wins! 91 // So, clear previous values, if any. 92 task.options.limitMods.clear(); 93 for (String mn : arg.split(",")) { 94 if (mn.isEmpty()) { 95 throw taskHelper.newBadArgs("err.mods.must.be.specified", 96 "--limit-modules"); 97 } 98 task.options.limitMods.add(mn); 99 } 100 }, "--limit-modules"), 101 new Option<JlinkTask>(true, (task, opt, arg) -> { 102 for (String mn : arg.split(",")) { 103 if (mn.isEmpty()) { 104 throw taskHelper.newBadArgs("err.mods.must.be.specified", 105 "--add-modules"); 106 } 107 task.options.addMods.add(mn); 108 } 109 }, "--add-modules"), 110 new Option<JlinkTask>(true, (task, opt, arg) -> { 111 Path path = Paths.get(arg); 112 task.options.output = path; 113 }, "--output"), 114 new Option<JlinkTask>(true, (task, opt, arg) -> { 115 String[] values = arg.split("="); 116 // check values 117 if (values.length != 2 || values[0].isEmpty() || values[1].isEmpty()) { 118 throw taskHelper.newBadArgs("err.launcher.value.format", arg); 119 } else { 120 String commandName = values[0]; 121 String moduleAndMain = values[1]; 122 int idx = moduleAndMain.indexOf("/"); 123 if (idx != -1) { 124 if (moduleAndMain.substring(0, idx).isEmpty()) { 125 throw taskHelper.newBadArgs("err.launcher.module.name.empty", arg); 126 } 127 128 if (moduleAndMain.substring(idx + 1).isEmpty()) { 129 throw taskHelper.newBadArgs("err.launcher.main.class.empty", arg); 130 } 131 } 132 task.options.launchers.put(commandName, moduleAndMain); 133 } 134 }, "--launcher"), 135 new Option<JlinkTask>(true, (task, opt, arg) -> { 136 if ("little".equals(arg)) { 137 task.options.endian = ByteOrder.LITTLE_ENDIAN; 138 } else if ("big".equals(arg)) { 139 task.options.endian = ByteOrder.BIG_ENDIAN; 140 } else { 141 throw taskHelper.newBadArgs("err.unknown.byte.order", arg); 142 } 143 }, "--endian"), 144 new Option<JlinkTask>(false, (task, opt, arg) -> { 145 task.options.version = true; 146 }, "--version"), 147 new Option<JlinkTask>(true, (task, opt, arg) -> { 148 Path path = Paths.get(arg); 149 if (Files.exists(path)) { 150 throw taskHelper.newBadArgs("err.dir.exists", path); 151 } 152 task.options.packagedModulesPath = path; 153 }, true, "--keep-packaged-modules"), 154 new Option<JlinkTask>(true, (task, opt, arg) -> { 155 task.options.saveoptsfile = arg; 156 }, "--save-opts"), 157 new Option<JlinkTask>(false, (task, opt, arg) -> { 158 task.options.fullVersion = true; 159 }, true, "--full-version"), 160 new Option<JlinkTask>(false, (task, opt, arg) -> { 161 task.options.ignoreSigning = true; 162 }, "--ignore-signing-information"),}; 163 164 private static final String PROGNAME = "jlink"; 165 private final OptionsValues options = new OptionsValues(); 166 167 private static final OptionsHelper<JlinkTask> optionsHelper 168 = taskHelper.newOptionsHelper(JlinkTask.class, recognizedOptions); 169 private PrintWriter log; 170 171 void setLog(PrintWriter out, PrintWriter err) { 172 log = out; 173 taskHelper.setLog(log); 174 } 175 176 /** 177 * Result codes. 178 */ 179 static final int 180 EXIT_OK = 0, // Completed with no errors. 181 EXIT_ERROR = 1, // Completed but reported errors. 182 EXIT_CMDERR = 2, // Bad command-line arguments 183 EXIT_SYSERR = 3, // System error or resource exhaustion. 184 EXIT_ABNORMAL = 4;// terminated abnormally 185 186 static class OptionsValues { 187 boolean help; 188 String saveoptsfile; 189 boolean version; 190 boolean fullVersion; 191 final List<Path> modulePath = new ArrayList<>(); 192 final Set<String> limitMods = new HashSet<>(); 193 final Set<String> addMods = new HashSet<>(); 194 Path output; 195 final Map<String, String> launchers = new HashMap<>(); 196 Path packagedModulesPath; 197 ByteOrder endian = ByteOrder.nativeOrder(); 198 boolean ignoreSigning = false; 199 } 200 201 int run(String[] args) { 202 if (log == null) { 203 setLog(new PrintWriter(System.out, true), 204 new PrintWriter(System.err, true)); 205 } 206 try { 207 optionsHelper.handleOptionsNoUnhandled(this, args); 208 if (options.help) { 209 optionsHelper.showHelp(PROGNAME); 210 return EXIT_OK; 211 } 212 if (optionsHelper.shouldListPlugins()) { 213 optionsHelper.listPlugins(); 214 return EXIT_OK; 215 } 216 if (options.version || options.fullVersion) { 217 taskHelper.showVersion(options.fullVersion); 218 return EXIT_OK; 219 } 220 221 if (taskHelper.getExistingImage() == null) { 222 if (options.modulePath.isEmpty()) { 223 throw taskHelper.newBadArgs("err.modulepath.must.be.specified").showUsage(true); 224 } 225 createImage(); 226 } else { 227 postProcessOnly(taskHelper.getExistingImage()); 228 } 229 230 if (options.saveoptsfile != null) { 231 Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes()); 232 } 233 234 return EXIT_OK; 235 } catch (PluginException | IllegalArgumentException | 236 UncheckedIOException |IOException | FindException | ResolutionException e) { 237 log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage()); 238 if (DEBUG) { 239 e.printStackTrace(log); 240 } 241 return EXIT_ERROR; 242 } catch (BadArgs e) { 243 taskHelper.reportError(e.key, e.args); 244 if (e.showUsage) { 245 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME)); 246 } 247 if (DEBUG) { 248 e.printStackTrace(log); 249 } 250 return EXIT_CMDERR; 251 } catch (Throwable x) { 252 log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage()); 253 x.printStackTrace(log); 254 return EXIT_ABNORMAL; 255 } finally { 256 log.flush(); 257 } 258 } 259 260 /* 261 * Jlink API entry point. 262 */ 263 public static void createImage(JlinkConfiguration config, 264 PluginsConfiguration plugins) 265 throws Exception { 266 Objects.requireNonNull(config); 267 Objects.requireNonNull(config.getOutput()); 268 plugins = plugins == null ? new PluginsConfiguration() : plugins; 269 270 if (config.getModulepaths().isEmpty()) { 271 throw new IllegalArgumentException("Empty module paths"); 272 } 273 274 ModuleFinder finder = newModuleFinder(config.getModulepaths(), 275 config.getLimitmods(), 276 config.getModules()); 277 278 if (config.getModules().isEmpty()) { 279 throw new IllegalArgumentException("No modules to add"); 280 } 281 282 // First create the image provider 283 ImageProvider imageProvider = 284 createImageProvider(finder, 285 config.getModules(), 286 config.getByteOrder(), 287 null, 288 IGNORE_SIGNING_DEFAULT, 289 null); 290 291 // Then create the Plugin Stack 292 ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins); 293 294 //Ask the stack to proceed; 295 stack.operate(imageProvider); 296 } 297 298 /* 299 * Jlink API entry point. 300 */ 301 public static void postProcessImage(ExecutableImage image, List<Plugin> postProcessorPlugins) 302 throws Exception { 303 Objects.requireNonNull(image); 304 Objects.requireNonNull(postProcessorPlugins); 305 PluginsConfiguration config = new PluginsConfiguration(postProcessorPlugins); 306 ImagePluginStack stack = ImagePluginConfiguration. 307 parseConfiguration(config); 308 309 stack.operate((ImagePluginStack stack1) -> image); 310 } 311 312 private void postProcessOnly(Path existingImage) throws Exception { 313 PluginsConfiguration config = taskHelper.getPluginsConfig(null, null); 314 ExecutableImage img = DefaultImageBuilder.getExecutableImage(existingImage); 315 if (img == null) { 316 throw taskHelper.newBadArgs("err.existing.image.invalid"); 317 } 318 postProcessImage(img, config.getPlugins()); 319 } 320 321 // the token for "all modules on the module path" 322 private static final String ALL_MODULE_PATH = "ALL-MODULE-PATH"; 323 private void createImage() throws Exception { 324 if (options.output == null) { 325 throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true); 326 } 327 328 if (options.addMods.isEmpty()) { 329 throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules") 330 .showUsage(true); 331 } 332 333 Set<String> roots = new HashSet<>(); 334 for (String mod : options.addMods) { 335 if (mod.equals(ALL_MODULE_PATH)) { 336 ModuleFinder finder = modulePathFinder(); 337 finder.findAll() 338 .stream() 339 .map(ModuleReference::descriptor) 340 .map(ModuleDescriptor::name) 341 .forEach(mn -> roots.add(mn)); 342 } else { 343 roots.add(mod); 344 } 345 } 346 347 ModuleFinder finder = newModuleFinder(options.modulePath, 348 options.limitMods, 349 roots); 350 351 352 // First create the image provider 353 ImageProvider imageProvider = createImageProvider(finder, 354 roots, 355 options.endian, 356 options.packagedModulesPath, 357 options.ignoreSigning, 358 log); 359 360 // Then create the Plugin Stack 361 ImagePluginStack stack = ImagePluginConfiguration. 362 parseConfiguration(taskHelper.getPluginsConfig(options.output, options.launchers)); 363 364 //Ask the stack to proceed 365 stack.operate(imageProvider); 366 } 367 368 /** 369 * Returns a module finder to find the observable modules specified in 370 * the --module-path and --limit-modules options 371 */ 372 private ModuleFinder modulePathFinder() { 373 Path[] entries = options.modulePath.toArray(new Path[0]); 374 ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries); 375 if (!options.limitMods.isEmpty()) { 376 finder = limitFinder(finder, options.limitMods, Collections.emptySet()); 377 } 378 return finder; 379 } 380 381 /* 382 * Returns a module finder of the given module path that limits 383 * the observable modules to those in the transitive closure of 384 * the modules specified in {@code limitMods} plus other modules 385 * specified in the {@code roots} set. 386 */ 387 public static ModuleFinder newModuleFinder(List<Path> paths, 388 Set<String> limitMods, 389 Set<String> roots) 390 { 391 Path[] entries = paths.toArray(new Path[0]); 392 ModuleFinder finder = ModulePath.of(Runtime.version(), true, entries); 393 394 // if limitmods is specified then limit the universe 395 if (!limitMods.isEmpty()) { 396 finder = limitFinder(finder, limitMods, roots); 397 } 398 return finder; 399 } 400 401 private static Path toPathLocation(ResolvedModule m) { 402 Optional<URI> ouri = m.reference().location(); 403 if (!ouri.isPresent()) 404 throw new InternalError(m + " does not have a location"); 405 URI uri = ouri.get(); 406 return Paths.get(uri); 407 } 408 409 private static ImageProvider createImageProvider(ModuleFinder finder, 410 Set<String> roots, 411 ByteOrder order, 412 Path retainModulesPath, 413 boolean ignoreSigning, 414 PrintWriter log) 415 throws IOException 416 { 417 if (roots.isEmpty()) { 418 throw new IllegalArgumentException("empty modules and limitmods"); 419 } 420 421 Configuration cf = Configuration.empty() 422 .resolve(finder, 423 ModuleFinder.of(), 424 roots); 425 426 // emit warning for modules that end with a digit 427 cf.modules().stream() 428 .map(ResolvedModule::name) 429 .filter(mn -> !Checks.hasLegalModuleNameLastCharacter(mn)) 430 .forEach(mn -> System.err.println("WARNING: Module name \"" 431 + mn + "\" may soon be illegal")); 432 433 // emit a warning for any incubating modules in the configuration 434 if (log != null) { 435 String im = cf.modules() 436 .stream() 437 .map(ResolvedModule::reference) 438 .filter(ModuleResolution::hasIncubatingWarning) 439 .map(ModuleReference::descriptor) 440 .map(ModuleDescriptor::name) 441 .collect(Collectors.joining(", ")); 442 443 if (!"".equals(im)) 444 log.println("WARNING: Using incubator modules: " + im); 445 } 446 447 Map<String, Path> mods = cf.modules().stream() 448 .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation)); 449 return new ImageHelper(cf, mods, order, retainModulesPath, ignoreSigning); 450 } 451 452 /* 453 * Returns a ModuleFinder that limits observability to the given root 454 * modules, their transitive dependences, plus a set of other modules. 455 */ 456 private static ModuleFinder limitFinder(ModuleFinder finder, 457 Set<String> roots, 458 Set<String> otherMods) { 459 460 // resolve all root modules 461 Configuration cf = Configuration.empty() 462 .resolve(finder, 463 ModuleFinder.of(), 464 roots); 465 466 // module name -> reference 467 Map<String, ModuleReference> map = new HashMap<>(); 468 cf.modules().forEach(m -> { 469 ModuleReference mref = m.reference(); 470 map.put(mref.descriptor().name(), mref); 471 }); 472 473 // add the other modules 474 otherMods.stream() 475 .map(finder::find) 476 .flatMap(Optional::stream) 477 .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref)); 478 479 // set of modules that are observable 480 Set<ModuleReference> mrefs = new HashSet<>(map.values()); 481 482 return new ModuleFinder() { 483 @Override 484 public Optional<ModuleReference> find(String name) { 485 return Optional.ofNullable(map.get(name)); 486 } 487 488 @Override 489 public Set<ModuleReference> findAll() { 490 return mrefs; 491 } 492 }; 493 } 494 495 private String getSaveOpts() { 496 StringBuilder sb = new StringBuilder(); 497 sb.append('#').append(new Date()).append("\n"); 498 for (String c : optionsHelper.getInputCommand()) { 499 sb.append(c).append(" "); 500 } 501 502 return sb.toString(); 503 } 504 505 private static String getBomHeader() { 506 StringBuilder sb = new StringBuilder(); 507 sb.append("#").append(new Date()).append("\n"); 508 sb.append("#Please DO NOT Modify this file").append("\n"); 509 return sb.toString(); 510 } 511 512 private String genBOMContent() throws IOException { 513 StringBuilder sb = new StringBuilder(); 514 sb.append(getBomHeader()); 515 StringBuilder command = new StringBuilder(); 516 for (String c : optionsHelper.getInputCommand()) { 517 command.append(c).append(" "); 518 } 519 sb.append("command").append(" = ").append(command); 520 sb.append("\n"); 521 522 return sb.toString(); 523 } 524 525 private static String genBOMContent(JlinkConfiguration config, 526 PluginsConfiguration plugins) 527 throws IOException { 528 StringBuilder sb = new StringBuilder(); 529 sb.append(getBomHeader()); 530 sb.append(config); 531 sb.append(plugins); 532 return sb.toString(); 533 } 534 535 private static class ImageHelper implements ImageProvider { 536 final ByteOrder order; 537 final Path packagedModulesPath; 538 final boolean ignoreSigning; 539 final Set<Archive> archives; 540 541 ImageHelper(Configuration cf, 542 Map<String, Path> modsPaths, 543 ByteOrder order, 544 Path packagedModulesPath, 545 boolean ignoreSigning) throws IOException { 546 this.order = order; 547 this.packagedModulesPath = packagedModulesPath; 548 this.ignoreSigning = ignoreSigning; 549 this.archives = modsPaths.entrySet().stream() 550 .map(e -> newArchive(e.getKey(), e.getValue())) 551 .collect(Collectors.toSet()); 552 } 553 554 private Archive newArchive(String module, Path path) { 555 if (path.toString().endsWith(".jmod")) { 556 return new JmodArchive(module, path); 557 } else if (path.toString().endsWith(".jar")) { 558 ModularJarArchive modularJarArchive = new ModularJarArchive(module, path); 559 560 Stream<Archive.Entry> signatures = modularJarArchive.entries().filter((entry) -> { 561 String name = entry.name().toUpperCase(Locale.ENGLISH); 562 563 return name.startsWith("META-INF/") && name.indexOf('/', 9) == -1 && ( 564 name.endsWith(".SF") || 565 name.endsWith(".DSA") || 566 name.endsWith(".RSA") || 567 name.endsWith(".EC") || 568 name.startsWith("META-INF/SIG-") 569 ); 570 }); 571 572 if (signatures.count() != 0) { 573 if (ignoreSigning) { 574 System.err.println(taskHelper.getMessage("warn.signing", path)); 575 } else { 576 throw new IllegalArgumentException(taskHelper.getMessage("err.signing", path)); 577 } 578 } 579 580 return modularJarArchive; 581 } else if (Files.isDirectory(path)) { 582 return new DirArchive(path); 583 } else { 584 throw new IllegalArgumentException( 585 taskHelper.getMessage("err.not.modular.format", module, path)); 586 } 587 } 588 589 @Override 590 public ExecutableImage retrieve(ImagePluginStack stack) throws IOException { 591 ExecutableImage image = ImageFileCreator.create(archives, order, stack); 592 if (packagedModulesPath != null) { 593 // copy the packaged modules to the given path 594 Files.createDirectories(packagedModulesPath); 595 for (Archive a : archives) { 596 Path file = a.getPath(); 597 Path dest = packagedModulesPath.resolve(file.getFileName()); 598 Files.copy(file, dest); 599 } 600 } 601 return image; 602 } 603 } 604 }