1 /* 2 * Copyright (c) 2015, 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 26 package jdk.tools.jlink.builder; 27 28 import java.io.BufferedOutputStream; 29 import java.io.BufferedWriter; 30 import java.io.ByteArrayInputStream; 31 import java.io.DataOutputStream; 32 import java.io.File; 33 import java.io.FileInputStream; 34 import java.io.FileOutputStream; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.OutputStream; 38 import java.io.OutputStreamWriter; 39 import java.io.UncheckedIOException; 40 import java.io.Writer; 41 import java.lang.module.ModuleDescriptor; 42 import java.nio.charset.StandardCharsets; 43 import java.nio.file.FileAlreadyExistsException; 44 import java.nio.file.Files; 45 import java.nio.file.Path; 46 import java.nio.file.Paths; 47 import java.nio.file.StandardOpenOption; 48 import java.nio.file.attribute.PosixFilePermission; 49 import java.util.Collections; 50 import java.util.HashMap; 51 import java.util.HashSet; 52 import java.util.List; 53 import java.util.Map; 54 import java.util.Objects; 55 import java.util.Optional; 56 import java.util.Properties; 57 import java.util.Set; 58 import static java.util.stream.Collectors.*; 59 60 import jdk.tools.jlink.internal.BasicImageWriter; 61 import jdk.tools.jlink.internal.plugins.FileCopierPlugin.SymImageFile; 62 import jdk.tools.jlink.internal.ExecutableImage; 63 import jdk.tools.jlink.plugin.ResourcePool; 64 import jdk.tools.jlink.plugin.ResourcePoolEntry; 65 import jdk.tools.jlink.plugin.ResourcePoolEntry.Type; 66 import jdk.tools.jlink.plugin.ResourcePoolModule; 67 import jdk.tools.jlink.plugin.PluginException; 68 69 /** 70 * 71 * Default Image Builder. This builder creates the default runtime image layout. 72 */ 73 public final class DefaultImageBuilder implements ImageBuilder { 74 // Top-level directory names in a modular runtime image 75 public static final String BIN_DIRNAME = "bin"; 76 public static final String CONF_DIRNAME = "conf"; 77 public static final String INCLUDE_DIRNAME = "include"; 78 public static final String LIB_DIRNAME = "lib"; 79 public static final String LEGAL_DIRNAME = "legal"; 80 public static final String MAN_DIRNAME = "man"; 81 82 /** 83 * The default java executable Image. 84 */ 85 static final class DefaultExecutableImage implements ExecutableImage { 86 87 private final Path home; 88 private final List<String> args; 89 private final Set<String> modules; 90 91 DefaultExecutableImage(Path home, Set<String> modules) { 92 Objects.requireNonNull(home); 93 if (!Files.exists(home)) { 94 throw new IllegalArgumentException("Invalid image home"); 95 } 96 this.home = home; 97 this.modules = Collections.unmodifiableSet(modules); 98 this.args = createArgs(home); 99 } 100 101 private static List<String> createArgs(Path home) { 102 Objects.requireNonNull(home); 103 Path binDir = home.resolve("bin"); 104 String java = Files.exists(binDir.resolve("java"))? "java" : "java.exe"; 105 return List.of(binDir.resolve(java).toString()); 106 } 107 108 @Override 109 public Path getHome() { 110 return home; 111 } 112 113 @Override 114 public Set<String> getModules() { 115 return modules; 116 } 117 118 @Override 119 public List<String> getExecutionArgs() { 120 return args; 121 } 122 123 @Override 124 public void storeLaunchArgs(List<String> args) { 125 try { 126 patchScripts(this, args); 127 } catch (IOException ex) { 128 throw new UncheckedIOException(ex); 129 } 130 } 131 } 132 133 private final Path root; 134 private final Path mdir; 135 private final Set<String> modules = new HashSet<>(); 136 private String targetOsName; 137 138 /** 139 * Default image builder constructor. 140 * 141 * @param root The image root directory. 142 * @throws IOException 143 */ 144 public DefaultImageBuilder(Path root) throws IOException { 145 Objects.requireNonNull(root); 146 147 this.root = root; 148 this.mdir = root.resolve("lib"); 149 Files.createDirectories(mdir); 150 } 151 152 private void storeFiles(Set<String> modules, Properties release) throws IOException { 153 if (release != null) { 154 addModules(release, modules); 155 File r = new File(root.toFile(), "release"); 156 try (FileOutputStream fo = new FileOutputStream(r)) { 157 release.store(fo, null); 158 } 159 } 160 } 161 162 private void addModules(Properties props, Set<String> modules) throws IOException { 163 StringBuilder builder = new StringBuilder(); 164 int i = 0; 165 for (String m : modules) { 166 builder.append(m); 167 if (i < modules.size() - 1) { 168 builder.append(","); 169 } 170 i++; 171 } 172 props.setProperty("MODULES", quote(builder.toString())); 173 } 174 175 @Override 176 public void storeFiles(ResourcePool files) { 177 try { 178 // populate release properties up-front. targetOsName 179 // field is assigned from there and used elsewhere. 180 Properties release = releaseProperties(files); 181 Path bin = root.resolve(BIN_DIRNAME); 182 183 // check any duplicated resource files 184 Map<Path, Set<String>> duplicates = new HashMap<>(); 185 files.entries() 186 .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE) 187 .collect(groupingBy(this::entryToImagePath, 188 mapping(ResourcePoolEntry::moduleName, toSet()))) 189 .entrySet() 190 .stream() 191 .filter(e -> e.getValue().size() > 1) 192 .forEach(e -> duplicates.put(e.getKey(), e.getValue())); 193 194 // write non-classes resource files to the image 195 files.entries() 196 .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE) 197 .forEach(f -> { 198 try { 199 accept(f); 200 } catch (FileAlreadyExistsException e) { 201 // error for duplicated entries 202 Path path = entryToImagePath(f); 203 UncheckedIOException x = 204 new UncheckedIOException(path + " duplicated in " + 205 duplicates.get(path), e); 206 x.addSuppressed(e); 207 throw x; 208 } catch (IOException ioExp) { 209 throw new UncheckedIOException(ioExp); 210 } 211 }); 212 213 files.moduleView().modules().forEach(m -> { 214 // Only add modules that contain packages 215 if (!m.packages().isEmpty()) { 216 modules.add(m.name()); 217 } 218 }); 219 220 storeFiles(modules, release); 221 222 if (root.getFileSystem().supportedFileAttributeViews() 223 .contains("posix")) { 224 // launchers in the bin directory need execute permission. 225 // On Windows, "bin" also subdirectories containing jvm.dll. 226 if (Files.isDirectory(bin)) { 227 Files.find(bin, 2, (path, attrs) -> { 228 return attrs.isRegularFile() && !path.toString().endsWith(".diz"); 229 }).forEach(this::setExecutable); 230 } 231 232 // jspawnhelper is in lib or lib/<arch> 233 Path lib = root.resolve(LIB_DIRNAME); 234 if (Files.isDirectory(lib)) { 235 Files.find(lib, 2, (path, attrs) -> { 236 return path.getFileName().toString().equals("jspawnhelper") 237 || path.getFileName().toString().equals("jexec"); 238 }).forEach(this::setExecutable); 239 } 240 241 // read-only legal notices/license files 242 Path legal = root.resolve(LEGAL_DIRNAME); 243 if (Files.isDirectory(legal)) { 244 Files.find(legal, 2, (path, attrs) -> { 245 return attrs.isRegularFile(); 246 }).forEach(this::setReadOnly); 247 } 248 } 249 250 // If native files are stripped completely, <root>/bin dir won't exist! 251 // So, don't bother generating launcher scripts. 252 if (Files.isDirectory(bin)) { 253 prepareApplicationFiles(files, modules); 254 } 255 } catch (IOException ex) { 256 throw new PluginException(ex); 257 } 258 } 259 260 // Parse version string and return a string that includes only version part 261 // leaving "pre", "build" information. See also: java.lang.Runtime.Version. 262 private static String parseVersion(String str) { 263 return Runtime.Version.parse(str). 264 version(). 265 stream(). 266 map(Object::toString). 267 collect(joining(".")); 268 } 269 270 private static String quote(String str) { 271 return "\"" + str + "\""; 272 } 273 274 private Properties releaseProperties(ResourcePool pool) throws IOException { 275 Properties props = new Properties(); 276 Optional<ResourcePoolModule> javaBase = pool.moduleView().findModule("java.base"); 277 javaBase.ifPresent(mod -> { 278 // fill release information available from transformed "java.base" module! 279 ModuleDescriptor desc = mod.descriptor(); 280 desc.osName().ifPresent(s -> { 281 props.setProperty("OS_NAME", quote(s)); 282 this.targetOsName = s; 283 }); 284 desc.osVersion().ifPresent(s -> props.setProperty("OS_VERSION", quote(s))); 285 desc.osArch().ifPresent(s -> props.setProperty("OS_ARCH", quote(s))); 286 desc.version().ifPresent(s -> props.setProperty("JAVA_VERSION", 287 quote(parseVersion(s.toString())))); 288 desc.version().ifPresent(s -> props.setProperty("JAVA_FULL_VERSION", 289 quote(s.toString()))); 290 }); 291 292 if (this.targetOsName == null) { 293 throw new PluginException("TargetPlatform attribute is missing for java.base module"); 294 } 295 296 Optional<ResourcePoolEntry> release = pool.findEntry("/java.base/release"); 297 if (release.isPresent()) { 298 try (InputStream is = release.get().content()) { 299 props.load(is); 300 } 301 } 302 303 return props; 304 } 305 306 /** 307 * Generates launcher scripts. 308 * 309 * @param imageContent The image content. 310 * @param modules The set of modules that the runtime image contains. 311 * @throws IOException 312 */ 313 protected void prepareApplicationFiles(ResourcePool imageContent, Set<String> modules) throws IOException { 314 // generate launch scripts for the modules with a main class 315 for (String module : modules) { 316 String path = "/" + module + "/module-info.class"; 317 Optional<ResourcePoolEntry> res = imageContent.findEntry(path); 318 if (!res.isPresent()) { 319 throw new IOException("module-info.class not found for " + module + " module"); 320 } 321 Optional<String> mainClass; 322 ByteArrayInputStream stream = new ByteArrayInputStream(res.get().contentBytes()); 323 mainClass = ModuleDescriptor.read(stream).mainClass(); 324 if (mainClass.isPresent()) { 325 Path cmd = root.resolve("bin").resolve(module); 326 // generate shell script for Unix platforms 327 StringBuilder sb = new StringBuilder(); 328 sb.append("#!/bin/sh") 329 .append("\n"); 330 sb.append("JLINK_VM_OPTIONS=") 331 .append("\n"); 332 sb.append("DIR=`dirname $0`") 333 .append("\n"); 334 sb.append("$DIR/java $JLINK_VM_OPTIONS -m ") 335 .append(module).append('/') 336 .append(mainClass.get()) 337 .append(" $@\n"); 338 339 try (BufferedWriter writer = Files.newBufferedWriter(cmd, 340 StandardCharsets.ISO_8859_1, 341 StandardOpenOption.CREATE_NEW)) { 342 writer.write(sb.toString()); 343 } 344 if (root.resolve("bin").getFileSystem() 345 .supportedFileAttributeViews().contains("posix")) { 346 setExecutable(cmd); 347 } 348 // generate .bat file for Windows 349 if (isWindows()) { 350 Path bat = root.resolve(BIN_DIRNAME).resolve(module + ".bat"); 351 sb = new StringBuilder(); 352 sb.append("@echo off") 353 .append("\r\n"); 354 sb.append("set JLINK_VM_OPTIONS=") 355 .append("\r\n"); 356 sb.append("set DIR=%~dp0") 357 .append("\r\n"); 358 sb.append("\"%DIR%\\java\" %JLINK_VM_OPTIONS% -m ") 359 .append(module).append('/') 360 .append(mainClass.get()) 361 .append(" %*\r\n"); 362 363 try (BufferedWriter writer = Files.newBufferedWriter(bat, 364 StandardCharsets.ISO_8859_1, 365 StandardOpenOption.CREATE_NEW)) { 366 writer.write(sb.toString()); 367 } 368 } 369 } 370 } 371 } 372 373 @Override 374 public DataOutputStream getJImageOutputStream() { 375 try { 376 Path jimageFile = mdir.resolve(BasicImageWriter.MODULES_IMAGE_NAME); 377 OutputStream fos = Files.newOutputStream(jimageFile); 378 BufferedOutputStream bos = new BufferedOutputStream(fos); 379 return new DataOutputStream(bos); 380 } catch (IOException ex) { 381 throw new UncheckedIOException(ex); 382 } 383 } 384 385 /** 386 * Returns the file name of this entry 387 */ 388 private String entryToFileName(ResourcePoolEntry entry) { 389 if (entry.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE) 390 throw new IllegalArgumentException("invalid type: " + entry); 391 392 String module = "/" + entry.moduleName() + "/"; 393 String filename = entry.path().substring(module.length()); 394 395 // Remove radical native|config|... 396 return filename.substring(filename.indexOf('/') + 1); 397 } 398 399 /** 400 * Returns the path of the given entry to be written in the image 401 */ 402 private Path entryToImagePath(ResourcePoolEntry entry) { 403 switch (entry.type()) { 404 case NATIVE_LIB: 405 String filename = entryToFileName(entry); 406 return Paths.get(nativeDir(filename), filename); 407 case NATIVE_CMD: 408 return Paths.get(BIN_DIRNAME, entryToFileName(entry)); 409 case CONFIG: 410 return Paths.get(CONF_DIRNAME, entryToFileName(entry)); 411 case HEADER_FILE: 412 return Paths.get(INCLUDE_DIRNAME, entryToFileName(entry)); 413 case MAN_PAGE: 414 return Paths.get(MAN_DIRNAME, entryToFileName(entry)); 415 case LEGAL_NOTICE: 416 return Paths.get(LEGAL_DIRNAME, entryToFileName(entry)); 417 case TOP: 418 return Paths.get(entryToFileName(entry)); 419 case OTHER: 420 return Paths.get("other", entryToFileName(entry)); 421 default: 422 throw new IllegalArgumentException("invalid type: " + entry); 423 } 424 } 425 426 private void accept(ResourcePoolEntry file) throws IOException { 427 if (file.linkedTarget() != null && file.type() != Type.LEGAL_NOTICE) { 428 throw new UnsupportedOperationException("symbolic link not implemented: " + file); 429 } 430 431 try (InputStream in = file.content()) { 432 switch (file.type()) { 433 case NATIVE_LIB: 434 Path dest = root.resolve(entryToImagePath(file)); 435 writeEntry(in, dest); 436 break; 437 case NATIVE_CMD: 438 Path p = root.resolve(entryToImagePath(file)); 439 writeEntry(in, p); 440 p.toFile().setExecutable(true); 441 break; 442 case CONFIG: 443 case HEADER_FILE: 444 case MAN_PAGE: 445 writeEntry(in, root.resolve(entryToImagePath(file))); 446 break; 447 case LEGAL_NOTICE: 448 Path source = entryToImagePath(file); 449 if (file.linkedTarget() == null) { 450 writeEntry(in, root.resolve(source)); 451 } else { 452 Path target = entryToImagePath(file.linkedTarget()); 453 Path relPath = source.getParent().relativize(target); 454 writeSymLinkEntry(root.resolve(source), relPath); 455 } 456 break; 457 case TOP: 458 break; 459 case OTHER: 460 String filename = entryToFileName(file); 461 if (file instanceof SymImageFile) { 462 SymImageFile sym = (SymImageFile) file; 463 Path target = root.resolve(sym.getTargetPath()); 464 if (!Files.exists(target)) { 465 throw new IOException("Sym link target " + target 466 + " doesn't exist"); 467 } 468 writeSymEntry(root.resolve(filename), target); 469 } else { 470 writeEntry(in, root.resolve(filename)); 471 } 472 break; 473 default: 474 throw new InternalError("unexpected entry: " + file.path()); 475 } 476 } 477 } 478 479 private void writeEntry(InputStream in, Path dstFile) throws IOException { 480 Objects.requireNonNull(in); 481 Objects.requireNonNull(dstFile); 482 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 483 Files.copy(in, dstFile); 484 } 485 486 private void writeSymEntry(Path dstFile, Path target) throws IOException { 487 Objects.requireNonNull(dstFile); 488 Objects.requireNonNull(target); 489 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 490 Files.createLink(dstFile, target); 491 } 492 493 /* 494 * Create a symbolic link to the given target if the target platform 495 * supports symbolic link; otherwise, it will create a tiny file 496 * to contain the path to the target. 497 */ 498 private void writeSymLinkEntry(Path dstFile, Path target) throws IOException { 499 Objects.requireNonNull(dstFile); 500 Objects.requireNonNull(target); 501 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 502 if (!isWindows() && root.getFileSystem() 503 .supportedFileAttributeViews() 504 .contains("posix")) { 505 Files.createSymbolicLink(dstFile, target); 506 } else { 507 try (BufferedWriter writer = Files.newBufferedWriter(dstFile)) { 508 writer.write(String.format("Please see %s%n", target.toString())); 509 } 510 } 511 } 512 513 private String nativeDir(String filename) { 514 if (isWindows()) { 515 if (filename.endsWith(".dll") || filename.endsWith(".diz") 516 || filename.endsWith(".pdb") || filename.endsWith(".map")) { 517 return BIN_DIRNAME; 518 } else { 519 return LIB_DIRNAME; 520 } 521 } else { 522 return LIB_DIRNAME; 523 } 524 } 525 526 private boolean isWindows() { 527 return targetOsName.startsWith("Windows"); 528 } 529 530 /** 531 * chmod ugo+x file 532 */ 533 private void setExecutable(Path file) { 534 try { 535 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 536 perms.add(PosixFilePermission.OWNER_EXECUTE); 537 perms.add(PosixFilePermission.GROUP_EXECUTE); 538 perms.add(PosixFilePermission.OTHERS_EXECUTE); 539 Files.setPosixFilePermissions(file, perms); 540 } catch (IOException ioe) { 541 throw new UncheckedIOException(ioe); 542 } 543 } 544 545 /** 546 * chmod ugo-w file 547 */ 548 private void setReadOnly(Path file) { 549 try { 550 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 551 perms.remove(PosixFilePermission.OWNER_WRITE); 552 perms.remove(PosixFilePermission.GROUP_WRITE); 553 perms.remove(PosixFilePermission.OTHERS_WRITE); 554 Files.setPosixFilePermissions(file, perms); 555 } catch (IOException ioe) { 556 throw new UncheckedIOException(ioe); 557 } 558 } 559 560 private static void createUtf8File(File file, String content) throws IOException { 561 try (OutputStream fout = new FileOutputStream(file); 562 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 563 output.write(content); 564 } 565 } 566 567 @Override 568 public ExecutableImage getExecutableImage() { 569 return new DefaultExecutableImage(root, modules); 570 } 571 572 // This is experimental, we should get rid-off the scripts in a near future 573 private static void patchScripts(ExecutableImage img, List<String> args) throws IOException { 574 Objects.requireNonNull(args); 575 if (!args.isEmpty()) { 576 Files.find(img.getHome().resolve(BIN_DIRNAME), 2, (path, attrs) -> { 577 return img.getModules().contains(path.getFileName().toString()); 578 }).forEach((p) -> { 579 try { 580 String pattern = "JLINK_VM_OPTIONS="; 581 byte[] content = Files.readAllBytes(p); 582 String str = new String(content, StandardCharsets.UTF_8); 583 int index = str.indexOf(pattern); 584 StringBuilder builder = new StringBuilder(); 585 if (index != -1) { 586 builder.append(str.substring(0, index)). 587 append(pattern); 588 for (String s : args) { 589 builder.append(s).append(" "); 590 } 591 String remain = str.substring(index + pattern.length()); 592 builder.append(remain); 593 str = builder.toString(); 594 try (BufferedWriter writer = Files.newBufferedWriter(p, 595 StandardCharsets.ISO_8859_1, 596 StandardOpenOption.WRITE)) { 597 writer.write(str); 598 } 599 } 600 } catch (IOException ex) { 601 throw new RuntimeException(ex); 602 } 603 }); 604 } 605 } 606 607 public static ExecutableImage getExecutableImage(Path root) { 608 Path binDir = root.resolve(BIN_DIRNAME); 609 if (Files.exists(binDir.resolve("java")) || 610 Files.exists(binDir.resolve("java.exe"))) { 611 return new DefaultExecutableImage(root, retrieveModules(root)); 612 } 613 return null; 614 } 615 616 private static Set<String> retrieveModules(Path root) { 617 Path releaseFile = root.resolve("release"); 618 Set<String> modules = new HashSet<>(); 619 if (Files.exists(releaseFile)) { 620 Properties release = new Properties(); 621 try (FileInputStream fi = new FileInputStream(releaseFile.toFile())) { 622 release.load(fi); 623 } catch (IOException ex) { 624 System.err.println("Can't read release file " + ex); 625 } 626 String mods = release.getProperty("MODULES"); 627 if (mods != null) { 628 String[] arr = mods.split(","); 629 for (String m : arr) { 630 modules.add(m.trim()); 631 } 632 633 } 634 } 635 return modules; 636 } 637 }