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) { 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 } 187 try { 188 optionsHelper.handleOptions(this, args); 189 if (options.help) { 190 optionsHelper.showHelp(PROGNAME); 191 return EXIT_OK; 192 } 193 if (optionsHelper.shouldListPlugins()) { 194 optionsHelper.listPlugins(); 195 return EXIT_OK; 196 } 197 if (options.version || options.fullVersion) { 198 taskHelper.showVersion(options.fullVersion); 199 return EXIT_OK; 200 } 201 if (taskHelper.getExistingImage() == null) { 202 if (options.modulePath == null || options.modulePath.isEmpty()) { 203 throw taskHelper.newBadArgs("err.modulepath.must.be.specified").showUsage(true); 204 } 205 createImage(); 206 } else { 207 postProcessOnly(taskHelper.getExistingImage()); 208 } 209 210 if (options.saveoptsfile != null) { 211 Files.write(Paths.get(options.saveoptsfile), getSaveOpts().getBytes()); 212 } 213 214 return EXIT_OK; 215 } catch (UncheckedIOException | PluginException | IllegalArgumentException | 216 IOException | ResolutionException e) { 217 log.println(taskHelper.getMessage("error.prefix") + " " + e.getMessage()); 218 if (DEBUG) { 219 e.printStackTrace(log); 220 } 221 return EXIT_ERROR; 222 } catch (BadArgs e) { 223 taskHelper.reportError(e.key, e.args); 224 if (e.showUsage) { 225 log.println(taskHelper.getMessage("main.usage.summary", PROGNAME)); 226 } 227 if (DEBUG) { 228 e.printStackTrace(log); 229 } 230 return EXIT_CMDERR; 231 } catch (Throwable x) { 232 log.println(taskHelper.getMessage("error.prefix") + " " + x.getMessage()); 233 x.printStackTrace(log); 234 return EXIT_ABNORMAL; 235 } finally { 236 log.flush(); 237 } 238 } 239 240 /* 241 * Jlink API entry point. 242 */ 243 public static void createImage(JlinkConfiguration config, 244 PluginsConfiguration plugins) 245 throws Exception { 246 Objects.requireNonNull(config); 247 Objects.requireNonNull(config.getOutput()); 248 plugins = plugins == null ? new PluginsConfiguration() : plugins; 249 250 if (config.getModulepaths().isEmpty()) { 251 throw new Exception("Empty module paths"); 252 } 253 254 ModuleFinder finder 255 = newModuleFinder(config.getModulepaths(), config.getLimitmods(), config.getModules()); 256 257 // First create the image provider 258 ImageProvider imageProvider 259 = createImageProvider(finder, 260 checkAddMods(config.getModules()), 261 config.getLimitmods(), 262 config.getByteOrder(), 263 null); 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 private void createImage() throws Exception { 296 if (options.output == null) { 297 throw taskHelper.newBadArgs("err.output.must.be.specified").showUsage(true); 298 } 299 ModuleFinder finder 300 = newModuleFinder(options.modulePath, options.limitMods, options.addMods); 301 try { 302 options.addMods = checkAddMods(options.addMods); 303 } catch (IllegalArgumentException ex) { 304 throw taskHelper.newBadArgs("err.mods.must.be.specified", "--add-modules") 305 .showUsage(true); 306 } 307 // First create the image provider 308 ImageProvider imageProvider 309 = createImageProvider(finder, 310 options.addMods, 311 options.limitMods, 312 options.endian, 313 options.packagedModulesPath); 314 315 // Then create the Plugin Stack 316 ImagePluginStack stack = ImagePluginConfiguration. 317 parseConfiguration(taskHelper.getPluginsConfig(options.output)); 318 319 //Ask the stack to proceed 320 stack.operate(imageProvider); 321 } 322 323 private static Set<String> checkAddMods(Set<String> addMods) { 324 if (addMods.isEmpty()) { 325 throw new IllegalArgumentException("no modules to add"); 326 } 327 return addMods; 328 } 329 330 public static ModuleFinder newModuleFinder(List<Path> paths, 331 Set<String> limitMods, 332 Set<String> addMods) 333 { 334 ModuleFinder finder = ModuleFinder.of(paths.toArray(new Path[0])); 335 336 // jmods are located at link-time 337 if (finder instanceof ConfigurableModuleFinder) { 338 ((ConfigurableModuleFinder) finder).configurePhase(Phase.LINK_TIME); 339 } 340 341 // if limitmods is specified then limit the universe 342 if (!limitMods.isEmpty()) { 343 finder = limitFinder(finder, limitMods, addMods); 344 } 345 return finder; 346 } 347 348 349 private static Path toPathLocation(ResolvedModule m) { 350 Optional<URI> ouri = m.reference().location(); 351 if (!ouri.isPresent()) 352 throw new InternalError(m + " does not have a location"); 353 URI uri = ouri.get(); 354 return Paths.get(uri); 355 } 356 357 private static ImageProvider createImageProvider(ModuleFinder finder, 358 Set<String> addMods, 359 Set<String> limitMods, 360 ByteOrder order, 361 Path retainModulesPath) 362 throws IOException 363 { 364 if (addMods.isEmpty()) { 365 throw new IllegalArgumentException("empty modules and limitmods"); 366 } 367 368 Configuration cf = Configuration.empty() 369 .resolveRequires(finder, 370 ModuleFinder.of(), 371 addMods); 372 373 Map<String, Path> mods = cf.modules().stream() 374 .collect(Collectors.toMap(ResolvedModule::name, JlinkTask::toPathLocation)); 375 return new ImageHelper(cf, mods, order, retainModulesPath); 376 } 377 378 /** 379 * Returns a ModuleFinder that limits observability to the given root 380 * modules, their transitive dependences, plus a set of other modules. 381 */ 382 private static ModuleFinder limitFinder(ModuleFinder finder, 383 Set<String> roots, 384 Set<String> otherMods) { 385 386 // resolve all root modules 387 Configuration cf = Configuration.empty() 388 .resolveRequires(finder, 389 ModuleFinder.of(), 390 roots); 391 392 // module name -> reference 393 Map<String, ModuleReference> map = new HashMap<>(); 394 cf.modules().forEach(m -> { 395 ModuleReference mref = m.reference(); 396 map.put(mref.descriptor().name(), mref); 397 }); 398 399 // add the other modules 400 otherMods.stream() 401 .map(finder::find) 402 .flatMap(Optional::stream) 403 .forEach(mref -> map.putIfAbsent(mref.descriptor().name(), mref)); 404 405 // set of modules that are observable 406 Set<ModuleReference> mrefs = new HashSet<>(map.values()); 407 408 return new ModuleFinder() { 409 @Override 410 public Optional<ModuleReference> find(String name) { 411 return Optional.ofNullable(map.get(name)); 412 } 413 414 @Override 415 public Set<ModuleReference> findAll() { 416 return mrefs; 417 } 418 }; 419 } 420 421 private String getSaveOpts() { 422 StringBuilder sb = new StringBuilder(); 423 sb.append('#').append(new Date()).append("\n"); 424 for (String c : optionsHelper.getInputCommand()) { 425 sb.append(c).append(" "); 426 } 427 428 return sb.toString(); 429 } 430 431 private static String getBomHeader() { 432 StringBuilder sb = new StringBuilder(); 433 sb.append("#").append(new Date()).append("\n"); 434 sb.append("#Please DO NOT Modify this file").append("\n"); 435 return sb.toString(); 436 } 437 438 private String genBOMContent() throws IOException { 439 StringBuilder sb = new StringBuilder(); 440 sb.append(getBomHeader()); 441 StringBuilder command = new StringBuilder(); 442 for (String c : optionsHelper.getInputCommand()) { 443 command.append(c).append(" "); 444 } 445 sb.append("command").append(" = ").append(command); 446 sb.append("\n"); 447 448 return sb.toString(); 449 } 450 451 private static String genBOMContent(JlinkConfiguration config, 452 PluginsConfiguration plugins) 453 throws IOException { 454 StringBuilder sb = new StringBuilder(); 455 sb.append(getBomHeader()); 456 sb.append(config); 457 sb.append(plugins); 458 return sb.toString(); 459 } 460 461 private static class ImageHelper implements ImageProvider { 462 463 final Set<Archive> archives; 464 final ByteOrder order; 465 final Path packagedModulesPath; 466 467 ImageHelper(Configuration cf, 468 Map<String, Path> modsPaths, 469 ByteOrder order, 470 Path packagedModulesPath) throws IOException { 471 archives = modsPaths.entrySet().stream() 472 .map(e -> newArchive(e.getKey(), e.getValue())) 473 .collect(Collectors.toSet()); 474 this.order = order; 475 this.packagedModulesPath = packagedModulesPath; 476 } 477 478 private Archive newArchive(String module, Path path) { 479 if (path.toString().endsWith(".jmod")) { 480 return new JmodArchive(module, path); 481 } else if (path.toString().endsWith(".jar")) { 482 return new ModularJarArchive(module, path); 483 } else if (Files.isDirectory(path)) { 484 return new DirArchive(path); 485 } else { 486 fail(RuntimeException.class, 487 "Selected module %s (%s) not in jmod or modular jar format", 488 module, 489 path); 490 } 491 return null; 492 } 493 494 @Override 495 public ExecutableImage retrieve(ImagePluginStack stack) throws IOException { 496 ExecutableImage image = ImageFileCreator.create(archives, order, stack); 497 if (packagedModulesPath != null) { 498 // copy the packaged modules to the given path 499 Files.createDirectories(packagedModulesPath); 500 for (Archive a : archives) { 501 Path file = a.getPath(); 502 Path dest = packagedModulesPath.resolve(file.getFileName()); 503 Files.copy(file, dest); 504 } 505 } 506 return image; 507 } 508 } 509 510 private static enum Section { 511 NATIVE_LIBS("native", nativeDir()), 512 NATIVE_CMDS("bin", "bin"), 513 CLASSES("classes", "classes"), 514 CONFIG("conf", "conf"), 515 UNKNOWN("unknown", "unknown"); 516 517 private static String nativeDir() { 518 if (System.getProperty("os.name").startsWith("Windows")) { 519 return "bin"; 520 } else { 521 return "lib"; 522 } 523 } 524 525 private final String jmodDir; 526 private final String imageDir; 527 528 Section(String jmodDir, String imageDir) { 529 this.jmodDir = jmodDir; 530 this.imageDir = imageDir; 531 } 532 533 String imageDir() { 534 return imageDir; 535 } 536 537 String jmodDir() { 538 return jmodDir; 539 } 540 541 boolean matches(String path) { 542 return path.startsWith(jmodDir); 543 } 544 545 static Section getSectionFromName(String dir) { 546 if (Section.NATIVE_LIBS.matches(dir)) { 547 return Section.NATIVE_LIBS; 548 } else if (Section.NATIVE_CMDS.matches(dir)) { 549 return Section.NATIVE_CMDS; 550 } else if (Section.CLASSES.matches(dir)) { 551 return Section.CLASSES; 552 } else if (Section.CONFIG.matches(dir)) { 553 return Section.CONFIG; 554 } else { 555 return Section.UNKNOWN; 556 } 557 } 558 } 559 }