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