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