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