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