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 storeRelease(ResourcePool pool) throws IOException { 153 Properties props = new Properties(); 154 Optional<ResourcePoolEntry> release = pool.findEntry("/java.base/release"); 155 if (release.isPresent()) { 156 try (InputStream is = release.get().content()) { 157 props.load(is); 158 } 159 } 160 File r = new File(root.toFile(), "release"); 161 try (FileOutputStream fo = new FileOutputStream(r)) { 162 props.store(fo, null); 163 } 164 } 165 166 @Override 167 public void storeFiles(ResourcePool files) { 168 try { 169 // populate targetOsName field up-front because it's used elsewhere. 170 Optional<ResourcePoolModule> javaBase = files.moduleView().findModule("java.base"); 171 javaBase.ifPresent(mod -> { 172 // fill release information available from transformed "java.base" module! 173 ModuleDescriptor desc = mod.descriptor(); 174 desc.osName().ifPresent(s -> { 175 this.targetOsName = s; 176 }); 177 }); 178 179 if (this.targetOsName == null) { 180 throw new PluginException("TargetPlatform attribute is missing for java.base module"); 181 } 182 183 // store 'release' file 184 storeRelease(files); 185 186 Path bin = root.resolve(BIN_DIRNAME); 187 188 // check any duplicated resource files 189 Map<Path, Set<String>> duplicates = new HashMap<>(); 190 files.entries() 191 .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE) 192 .collect(groupingBy(this::entryToImagePath, 193 mapping(ResourcePoolEntry::moduleName, toSet()))) 194 .entrySet() 195 .stream() 196 .filter(e -> e.getValue().size() > 1) 197 .forEach(e -> duplicates.put(e.getKey(), e.getValue())); 198 199 // write non-classes resource files to the image 200 files.entries() 201 .filter(f -> f.type() != ResourcePoolEntry.Type.CLASS_OR_RESOURCE) 202 .forEach(f -> { 203 try { 204 accept(f); 205 } catch (FileAlreadyExistsException e) { 206 // error for duplicated entries 207 Path path = entryToImagePath(f); 208 UncheckedIOException x = 209 new UncheckedIOException(path + " duplicated in " + 210 duplicates.get(path), e); 211 x.addSuppressed(e); 212 throw x; 213 } catch (IOException ioExp) { 214 throw new UncheckedIOException(ioExp); 215 } 216 }); 217 218 files.moduleView().modules().forEach(m -> { 219 // Only add modules that contain packages 220 if (!m.packages().isEmpty()) { 221 modules.add(m.name()); 222 } 223 }); 224 225 if (root.getFileSystem().supportedFileAttributeViews() 226 .contains("posix")) { 227 // launchers in the bin directory need execute permission. 228 // On Windows, "bin" also subdirectories containing jvm.dll. 229 if (Files.isDirectory(bin)) { 230 Files.find(bin, 2, (path, attrs) -> { 231 return attrs.isRegularFile() && !path.toString().endsWith(".diz"); 232 }).forEach(this::setExecutable); 233 } 234 235 // jspawnhelper is in lib or lib/<arch> 236 Path lib = root.resolve(LIB_DIRNAME); 237 if (Files.isDirectory(lib)) { 238 Files.find(lib, 2, (path, attrs) -> { 239 return path.getFileName().toString().equals("jspawnhelper") 240 || path.getFileName().toString().equals("jexec"); 241 }).forEach(this::setExecutable); 242 } 243 244 // read-only legal notices/license files 245 Path legal = root.resolve(LEGAL_DIRNAME); 246 if (Files.isDirectory(legal)) { 247 Files.find(legal, 2, (path, attrs) -> { 248 return attrs.isRegularFile(); 249 }).forEach(this::setReadOnly); 250 } 251 } 252 253 // If native files are stripped completely, <root>/bin dir won't exist! 254 // So, don't bother generating launcher scripts. 255 if (Files.isDirectory(bin)) { 256 prepareApplicationFiles(files, modules); 257 } 258 } catch (IOException ex) { 259 throw new PluginException(ex); 260 } 261 } 262 263 /** 264 * Generates launcher scripts. 265 * 266 * @param imageContent The image content. 267 * @param modules The set of modules that the runtime image contains. 268 * @throws IOException 269 */ 270 protected void prepareApplicationFiles(ResourcePool imageContent, Set<String> modules) throws IOException { 271 // generate launch scripts for the modules with a main class 272 for (String module : modules) { 273 String path = "/" + module + "/module-info.class"; 274 Optional<ResourcePoolEntry> res = imageContent.findEntry(path); 275 if (!res.isPresent()) { 276 throw new IOException("module-info.class not found for " + module + " module"); 277 } 278 Optional<String> mainClass; 279 ByteArrayInputStream stream = new ByteArrayInputStream(res.get().contentBytes()); 280 mainClass = ModuleDescriptor.read(stream).mainClass(); 281 if (mainClass.isPresent()) { 282 Path cmd = root.resolve("bin").resolve(module); 283 // generate shell script for Unix platforms 284 StringBuilder sb = new StringBuilder(); 285 sb.append("#!/bin/sh") 286 .append("\n"); 287 sb.append("JLINK_VM_OPTIONS=") 288 .append("\n"); 289 sb.append("DIR=`dirname $0`") 290 .append("\n"); 291 sb.append("$DIR/java $JLINK_VM_OPTIONS -m ") 292 .append(module).append('/') 293 .append(mainClass.get()) 294 .append(" $@\n"); 295 296 try (BufferedWriter writer = Files.newBufferedWriter(cmd, 297 StandardCharsets.ISO_8859_1, 298 StandardOpenOption.CREATE_NEW)) { 299 writer.write(sb.toString()); 300 } 301 if (root.resolve("bin").getFileSystem() 302 .supportedFileAttributeViews().contains("posix")) { 303 setExecutable(cmd); 304 } 305 // generate .bat file for Windows 306 if (isWindows()) { 307 Path bat = root.resolve(BIN_DIRNAME).resolve(module + ".bat"); 308 sb = new StringBuilder(); 309 sb.append("@echo off") 310 .append("\r\n"); 311 sb.append("set JLINK_VM_OPTIONS=") 312 .append("\r\n"); 313 sb.append("set DIR=%~dp0") 314 .append("\r\n"); 315 sb.append("\"%DIR%\\java\" %JLINK_VM_OPTIONS% -m ") 316 .append(module).append('/') 317 .append(mainClass.get()) 318 .append(" %*\r\n"); 319 320 try (BufferedWriter writer = Files.newBufferedWriter(bat, 321 StandardCharsets.ISO_8859_1, 322 StandardOpenOption.CREATE_NEW)) { 323 writer.write(sb.toString()); 324 } 325 } 326 } 327 } 328 } 329 330 @Override 331 public DataOutputStream getJImageOutputStream() { 332 try { 333 Path jimageFile = mdir.resolve(BasicImageWriter.MODULES_IMAGE_NAME); 334 OutputStream fos = Files.newOutputStream(jimageFile); 335 BufferedOutputStream bos = new BufferedOutputStream(fos); 336 return new DataOutputStream(bos); 337 } catch (IOException ex) { 338 throw new UncheckedIOException(ex); 339 } 340 } 341 342 /** 343 * Returns the file name of this entry 344 */ 345 private String entryToFileName(ResourcePoolEntry entry) { 346 if (entry.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE) 347 throw new IllegalArgumentException("invalid type: " + entry); 348 349 String module = "/" + entry.moduleName() + "/"; 350 String filename = entry.path().substring(module.length()); 351 352 // Remove radical native|config|... 353 return filename.substring(filename.indexOf('/') + 1); 354 } 355 356 /** 357 * Returns the path of the given entry to be written in the image 358 */ 359 private Path entryToImagePath(ResourcePoolEntry entry) { 360 switch (entry.type()) { 361 case NATIVE_LIB: 362 String filename = entryToFileName(entry); 363 return Paths.get(nativeDir(filename), filename); 364 case NATIVE_CMD: 365 return Paths.get(BIN_DIRNAME, entryToFileName(entry)); 366 case CONFIG: 367 return Paths.get(CONF_DIRNAME, entryToFileName(entry)); 368 case HEADER_FILE: 369 return Paths.get(INCLUDE_DIRNAME, entryToFileName(entry)); 370 case MAN_PAGE: 371 return Paths.get(MAN_DIRNAME, entryToFileName(entry)); 372 case LEGAL_NOTICE: 373 return Paths.get(LEGAL_DIRNAME, entryToFileName(entry)); 374 case TOP: 375 return Paths.get(entryToFileName(entry)); 376 case OTHER: 377 return Paths.get("other", entryToFileName(entry)); 378 default: 379 throw new IllegalArgumentException("invalid type: " + entry); 380 } 381 } 382 383 private void accept(ResourcePoolEntry file) throws IOException { 384 if (file.linkedTarget() != null && file.type() != Type.LEGAL_NOTICE) { 385 throw new UnsupportedOperationException("symbolic link not implemented: " + file); 386 } 387 388 try (InputStream in = file.content()) { 389 switch (file.type()) { 390 case NATIVE_LIB: 391 Path dest = root.resolve(entryToImagePath(file)); 392 writeEntry(in, dest); 393 break; 394 case NATIVE_CMD: 395 Path p = root.resolve(entryToImagePath(file)); 396 writeEntry(in, p); 397 p.toFile().setExecutable(true); 398 break; 399 case CONFIG: 400 case HEADER_FILE: 401 case MAN_PAGE: 402 writeEntry(in, root.resolve(entryToImagePath(file))); 403 break; 404 case LEGAL_NOTICE: 405 Path source = entryToImagePath(file); 406 if (file.linkedTarget() == null) { 407 writeEntry(in, root.resolve(source)); 408 } else { 409 Path target = entryToImagePath(file.linkedTarget()); 410 Path relPath = source.getParent().relativize(target); 411 writeSymLinkEntry(root.resolve(source), relPath); 412 } 413 break; 414 case TOP: 415 break; 416 case OTHER: 417 String filename = entryToFileName(file); 418 if (file instanceof SymImageFile) { 419 SymImageFile sym = (SymImageFile) file; 420 Path target = root.resolve(sym.getTargetPath()); 421 if (!Files.exists(target)) { 422 throw new IOException("Sym link target " + target 423 + " doesn't exist"); 424 } 425 writeSymEntry(root.resolve(filename), target); 426 } else { 427 writeEntry(in, root.resolve(filename)); 428 } 429 break; 430 default: 431 throw new InternalError("unexpected entry: " + file.path()); 432 } 433 } 434 } 435 436 private void writeEntry(InputStream in, Path dstFile) throws IOException { 437 Objects.requireNonNull(in); 438 Objects.requireNonNull(dstFile); 439 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 440 Files.copy(in, dstFile); 441 } 442 443 private void writeSymEntry(Path dstFile, Path target) throws IOException { 444 Objects.requireNonNull(dstFile); 445 Objects.requireNonNull(target); 446 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 447 Files.createLink(dstFile, target); 448 } 449 450 /* 451 * Create a symbolic link to the given target if the target platform 452 * supports symbolic link; otherwise, it will create a tiny file 453 * to contain the path to the target. 454 */ 455 private void writeSymLinkEntry(Path dstFile, Path target) throws IOException { 456 Objects.requireNonNull(dstFile); 457 Objects.requireNonNull(target); 458 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 459 if (!isWindows() && root.getFileSystem() 460 .supportedFileAttributeViews() 461 .contains("posix")) { 462 Files.createSymbolicLink(dstFile, target); 463 } else { 464 try (BufferedWriter writer = Files.newBufferedWriter(dstFile)) { 465 writer.write(String.format("Please see %s%n", target.toString())); 466 } 467 } 468 } 469 470 private String nativeDir(String filename) { 471 if (isWindows()) { 472 if (filename.endsWith(".dll") || filename.endsWith(".diz") 473 || filename.endsWith(".pdb") || filename.endsWith(".map")) { 474 return BIN_DIRNAME; 475 } else { 476 return LIB_DIRNAME; 477 } 478 } else { 479 return LIB_DIRNAME; 480 } 481 } 482 483 private boolean isWindows() { 484 return targetOsName.startsWith("Windows"); 485 } 486 487 /** 488 * chmod ugo+x file 489 */ 490 private void setExecutable(Path file) { 491 try { 492 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 493 perms.add(PosixFilePermission.OWNER_EXECUTE); 494 perms.add(PosixFilePermission.GROUP_EXECUTE); 495 perms.add(PosixFilePermission.OTHERS_EXECUTE); 496 Files.setPosixFilePermissions(file, perms); 497 } catch (IOException ioe) { 498 throw new UncheckedIOException(ioe); 499 } 500 } 501 502 /** 503 * chmod ugo-w file 504 */ 505 private void setReadOnly(Path file) { 506 try { 507 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 508 perms.remove(PosixFilePermission.OWNER_WRITE); 509 perms.remove(PosixFilePermission.GROUP_WRITE); 510 perms.remove(PosixFilePermission.OTHERS_WRITE); 511 Files.setPosixFilePermissions(file, perms); 512 } catch (IOException ioe) { 513 throw new UncheckedIOException(ioe); 514 } 515 } 516 517 private static void createUtf8File(File file, String content) throws IOException { 518 try (OutputStream fout = new FileOutputStream(file); 519 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 520 output.write(content); 521 } 522 } 523 524 @Override 525 public ExecutableImage getExecutableImage() { 526 return new DefaultExecutableImage(root, modules); 527 } 528 529 // This is experimental, we should get rid-off the scripts in a near future 530 private static void patchScripts(ExecutableImage img, List<String> args) throws IOException { 531 Objects.requireNonNull(args); 532 if (!args.isEmpty()) { 533 Files.find(img.getHome().resolve(BIN_DIRNAME), 2, (path, attrs) -> { 534 return img.getModules().contains(path.getFileName().toString()); 535 }).forEach((p) -> { 536 try { 537 String pattern = "JLINK_VM_OPTIONS="; 538 byte[] content = Files.readAllBytes(p); 539 String str = new String(content, StandardCharsets.UTF_8); 540 int index = str.indexOf(pattern); 541 StringBuilder builder = new StringBuilder(); 542 if (index != -1) { 543 builder.append(str.substring(0, index)). 544 append(pattern); 545 for (String s : args) { 546 builder.append(s).append(" "); 547 } 548 String remain = str.substring(index + pattern.length()); 549 builder.append(remain); 550 str = builder.toString(); 551 try (BufferedWriter writer = Files.newBufferedWriter(p, 552 StandardCharsets.ISO_8859_1, 553 StandardOpenOption.WRITE)) { 554 writer.write(str); 555 } 556 } 557 } catch (IOException ex) { 558 throw new RuntimeException(ex); 559 } 560 }); 561 } 562 } 563 564 public static ExecutableImage getExecutableImage(Path root) { 565 Path binDir = root.resolve(BIN_DIRNAME); 566 if (Files.exists(binDir.resolve("java")) || 567 Files.exists(binDir.resolve("java.exe"))) { 568 return new DefaultExecutableImage(root, retrieveModules(root)); 569 } 570 return null; 571 } 572 573 private static Set<String> retrieveModules(Path root) { 574 Path releaseFile = root.resolve("release"); 575 Set<String> modules = new HashSet<>(); 576 if (Files.exists(releaseFile)) { 577 Properties release = new Properties(); 578 try (FileInputStream fi = new FileInputStream(releaseFile.toFile())) { 579 release.load(fi); 580 } catch (IOException ex) { 581 System.err.println("Can't read release file " + ex); 582 } 583 String mods = release.getProperty("MODULES"); 584 if (mods != null) { 585 String[] arr = mods.substring(1, mods.length() - 1).split(" "); 586 for (String m : arr) { 587 modules.add(m.trim()); 588 } 589 590 } 591 } 592 return modules; 593 } 594 }