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