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