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