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