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