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