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