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.UncheckedIOException; 41 import java.io.Writer; 42 import java.lang.module.ModuleDescriptor; 43 import java.nio.charset.StandardCharsets; 44 import java.nio.file.Files; 45 import java.nio.file.Path; 46 import java.nio.file.StandardOpenOption; 47 import java.nio.file.attribute.PosixFileAttributeView; 48 import java.nio.file.attribute.PosixFilePermission; 49 import java.util.ArrayList; 50 import java.util.Collections; 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 jdk.tools.jlink.internal.BasicImageWriter; 59 import jdk.tools.jlink.internal.plugins.FileCopierPlugin; 60 import jdk.tools.jlink.internal.plugins.FileCopierPlugin.SymImageFile; 61 import jdk.tools.jlink.internal.ExecutableImage; 62 import jdk.tools.jlink.plugin.ResourcePool; 63 import jdk.tools.jlink.plugin.ResourcePoolEntry; 64 import jdk.tools.jlink.plugin.ResourcePoolModule; 65 import jdk.tools.jlink.plugin.PluginException; 66 67 /** 68 * 69 * Default Image Builder. This builder creates the default runtime image layout. 70 */ 71 public final class DefaultImageBuilder implements ImageBuilder { 72 73 /** 74 * The default java executable Image. 75 */ 76 static final class DefaultExecutableImage implements ExecutableImage { 77 78 private final Path home; 79 private final List<String> args; 80 private final Set<String> modules; 81 82 public DefaultExecutableImage(Path home, Set<String> modules) { 83 this(home, modules, createArgs(home)); 84 } 85 86 private DefaultExecutableImage(Path home, Set<String> modules, 87 List<String> args) { 88 Objects.requireNonNull(home); 89 Objects.requireNonNull(args); 90 if (!Files.exists(home)) { 91 throw new IllegalArgumentException("Invalid image home"); 92 } 93 this.home = home; 94 this.modules = Collections.unmodifiableSet(modules); 95 this.args = Collections.unmodifiableList(args); 96 } 97 98 private static List<String> createArgs(Path home) { 99 Objects.requireNonNull(home); 100 List<String> javaArgs = new ArrayList<>(); 101 javaArgs.add(home.resolve("bin"). 102 resolve(getJavaProcessName()).toString()); 103 return javaArgs; 104 } 105 106 @Override 107 public Path getHome() { 108 return home; 109 } 110 111 @Override 112 public Set<String> getModules() { 113 return modules; 114 } 115 116 @Override 117 public List<String> getExecutionArgs() { 118 return args; 119 } 120 121 @Override 122 public void storeLaunchArgs(List<String> args) { 123 try { 124 patchScripts(this, args); 125 } catch (IOException ex) { 126 throw new UncheckedIOException(ex); 127 } 128 } 129 } 130 131 private final Path root; 132 private final Path mdir; 133 private final Set<String> modules = new HashSet<>(); 134 135 /** 136 * Default image builder constructor. 137 * 138 * @param root The image root directory. 139 * @throws IOException 140 */ 141 public DefaultImageBuilder(Path root) throws IOException { 142 Objects.requireNonNull(root); 143 144 this.root = root; 145 this.mdir = root.resolve("lib"); 146 Files.createDirectories(mdir); 147 } 148 149 private void storeFiles(Set<String> modules, Properties release) throws IOException { 150 if (release != null) { 151 addModules(release, modules); 152 File r = new File(root.toFile(), "release"); 153 try (FileOutputStream fo = new FileOutputStream(r)) { 154 release.store(fo, null); 155 } 156 } 157 } 158 159 private void addModules(Properties props, Set<String> modules) throws IOException { 160 StringBuilder builder = new StringBuilder(); 161 int i = 0; 162 for (String m : modules) { 163 builder.append(m); 164 if (i < modules.size() - 1) { 165 builder.append(","); 166 } 167 i++; 168 } 169 props.setProperty("MODULES", builder.toString()); 170 } 171 172 @Override 173 public void storeFiles(ResourcePool files) { 174 try { 175 files.entries().forEach(f -> { 176 if (!f.type().equals(ResourcePoolEntry.Type.CLASS_OR_RESOURCE)) { 177 try { 178 accept(f); 179 } catch (IOException ioExp) { 180 throw new UncheckedIOException(ioExp); 181 } 182 } 183 }); 184 files.moduleView().modules().forEach(m -> { 185 // Only add modules that contain packages 186 if (!m.packages().isEmpty()) { 187 // Skip the fake module used by FileCopierPlugin when copying files. 188 if (m.name().equals(FileCopierPlugin.FAKE_MODULE)) { 189 return; 190 } 191 modules.add(m.name()); 192 } 193 }); 194 storeFiles(modules, releaseProperties(files)); 195 196 if (Files.getFileStore(root).supportsFileAttributeView(PosixFileAttributeView.class)) { 197 // launchers in the bin directory need execute permission 198 Path bin = root.resolve("bin"); 199 if (Files.isDirectory(bin)) { 200 Files.list(bin) 201 .filter(f -> !f.toString().endsWith(".diz")) 202 .filter(f -> Files.isRegularFile(f)) 203 .forEach(this::setExecutable); 204 } 205 206 // jspawnhelper is in lib or lib/<arch> 207 Path lib = root.resolve("lib"); 208 if (Files.isDirectory(lib)) { 209 Files.find(lib, 2, (path, attrs) -> { 210 return path.getFileName().toString().equals("jspawnhelper") 211 || path.getFileName().toString().equals("jexec"); 212 }).forEach(this::setExecutable); 213 } 214 } 215 216 prepareApplicationFiles(files, modules); 217 } catch (IOException ex) { 218 throw new PluginException(ex); 219 } 220 } 221 222 private Properties releaseProperties(ResourcePool pool) throws IOException { 223 Properties props = new Properties(); 224 Optional<ResourcePoolModule> javaBase = pool.moduleView().findModule("java.base"); 225 javaBase.ifPresent(mod -> { 226 // fill release information available from transformed "java.base" module! 227 ModuleDescriptor desc = mod.descriptor(); 228 desc.osName().ifPresent(s -> props.setProperty("OS_NAME", s)); 229 desc.osVersion().ifPresent(s -> props.setProperty("OS_VERSION", s)); 230 desc.osArch().ifPresent(s -> props.setProperty("OS_ARCH", s)); 231 props.setProperty("JAVA_VERSION", System.getProperty("java.version")); 232 }); 233 234 Optional<ResourcePoolEntry> release = pool.findEntry("/java.base/release"); 235 if (release.isPresent()) { 236 try (InputStream is = release.get().content()) { 237 props.load(is); 238 } 239 } 240 241 return props; 242 } 243 244 /** 245 * Generates launcher scripts. 246 * 247 * @param imageContent The image content. 248 * @param modules The set of modules that the runtime image contains. 249 * @throws IOException 250 */ 251 protected void prepareApplicationFiles(ResourcePool imageContent, Set<String> modules) throws IOException { 252 // generate launch scripts for the modules with a main class 253 for (String module : modules) { 254 String path = "/" + module + "/module-info.class"; 255 Optional<ResourcePoolEntry> res = imageContent.findEntry(path); 256 if (!res.isPresent()) { 257 throw new IOException("module-info.class not found for " + module + " module"); 258 } 259 Optional<String> mainClass; 260 ByteArrayInputStream stream = new ByteArrayInputStream(res.get().contentBytes()); 261 mainClass = ModuleDescriptor.read(stream).mainClass(); 262 if (mainClass.isPresent()) { 263 Path cmd = root.resolve("bin").resolve(module); 264 // generate shell script for Unix platforms 265 StringBuilder sb = new StringBuilder(); 266 sb.append("#!/bin/sh") 267 .append("\n"); 268 sb.append("JLINK_VM_OPTIONS=") 269 .append("\n"); 270 sb.append("DIR=`dirname $0`") 271 .append("\n"); 272 sb.append("$DIR/java $JLINK_VM_OPTIONS -m ") 273 .append(module).append('/') 274 .append(mainClass.get()) 275 .append(" $@\n"); 276 277 try (BufferedWriter writer = Files.newBufferedWriter(cmd, 278 StandardCharsets.ISO_8859_1, 279 StandardOpenOption.CREATE_NEW)) { 280 writer.write(sb.toString()); 281 } 282 if (Files.getFileStore(root.resolve("bin")) 283 .supportsFileAttributeView(PosixFileAttributeView.class)) { 284 setExecutable(cmd); 285 } 286 // generate .bat file for Windows 287 if (isWindows()) { 288 Path bat = root.resolve("bin").resolve(module + ".bat"); 289 sb = new StringBuilder(); 290 sb.append("@echo off") 291 .append("\r\n"); 292 sb.append("set JLINK_VM_OPTIONS=") 293 .append("\r\n"); 294 sb.append("set DIR=%~dp0") 295 .append("\r\n"); 296 sb.append("\"%DIR%\\java\" %JLINK_VM_OPTIONS% -m ") 297 .append(module).append('/') 298 .append(mainClass.get()) 299 .append(" %*\r\n"); 300 301 try (BufferedWriter writer = Files.newBufferedWriter(bat, 302 StandardCharsets.ISO_8859_1, 303 StandardOpenOption.CREATE_NEW)) { 304 writer.write(sb.toString()); 305 } 306 } 307 } 308 } 309 } 310 311 @Override 312 public DataOutputStream getJImageOutputStream() { 313 try { 314 Path jimageFile = mdir.resolve(BasicImageWriter.MODULES_IMAGE_NAME); 315 OutputStream fos = Files.newOutputStream(jimageFile); 316 BufferedOutputStream bos = new BufferedOutputStream(fos); 317 return new DataOutputStream(bos); 318 } catch (IOException ex) { 319 throw new UncheckedIOException(ex); 320 } 321 } 322 323 private void accept(ResourcePoolEntry file) throws IOException { 324 String fullPath = file.path(); 325 String module = "/" + file.moduleName() + "/"; 326 String filename = fullPath.substring(module.length()); 327 // Remove radical native|config|... 328 filename = filename.substring(filename.indexOf('/') + 1); 329 try (InputStream in = file.content()) { 330 switch (file.type()) { 331 case NATIVE_LIB: 332 writeEntry(in, destFile(nativeDir(filename), filename)); 333 break; 334 case NATIVE_CMD: 335 Path path = destFile("bin", filename); 336 writeEntry(in, path); 337 path.toFile().setExecutable(true); 338 break; 339 case CONFIG: 340 writeEntry(in, destFile("conf", filename)); 341 break; 342 case TOP: 343 break; 344 case OTHER: 345 if (file instanceof SymImageFile) { 346 SymImageFile sym = (SymImageFile) file; 347 Path target = root.resolve(sym.getTargetPath()); 348 if (!Files.exists(target)) { 349 throw new IOException("Sym link target " + target 350 + " doesn't exist"); 351 } 352 writeSymEntry(root.resolve(filename), target); 353 } else { 354 writeEntry(in, root.resolve(filename)); 355 } 356 break; 357 default: 358 throw new InternalError("unexpected entry: " + fullPath); 359 } 360 } 361 } 362 363 private Path destFile(String dir, String filename) { 364 return root.resolve(dir).resolve(filename); 365 } 366 367 private void writeEntry(InputStream in, Path dstFile) throws IOException { 368 Objects.requireNonNull(in); 369 Objects.requireNonNull(dstFile); 370 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 371 Files.copy(in, dstFile); 372 } 373 374 private void writeSymEntry(Path dstFile, Path target) throws IOException { 375 Objects.requireNonNull(dstFile); 376 Objects.requireNonNull(target); 377 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 378 Files.createLink(dstFile, target); 379 } 380 381 private static String nativeDir(String filename) { 382 if (isWindows()) { 383 if (filename.endsWith(".dll") || filename.endsWith(".diz") 384 || filename.endsWith(".pdb") || filename.endsWith(".map")) { 385 return "bin"; 386 } else { 387 return "lib"; 388 } 389 } else { 390 return "lib"; 391 } 392 } 393 394 private static boolean isWindows() { 395 return System.getProperty("os.name").startsWith("Windows"); 396 } 397 398 /** 399 * chmod ugo+x file 400 */ 401 private void setExecutable(Path file) { 402 try { 403 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 404 perms.add(PosixFilePermission.OWNER_EXECUTE); 405 perms.add(PosixFilePermission.GROUP_EXECUTE); 406 perms.add(PosixFilePermission.OTHERS_EXECUTE); 407 Files.setPosixFilePermissions(file, perms); 408 } catch (IOException ioe) { 409 throw new UncheckedIOException(ioe); 410 } 411 } 412 413 private static void createUtf8File(File file, String content) throws IOException { 414 try (OutputStream fout = new FileOutputStream(file); 415 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 416 output.write(content); 417 } 418 } 419 420 @Override 421 public ExecutableImage getExecutableImage() { 422 return new DefaultExecutableImage(root, modules); 423 } 424 425 // This is experimental, we should get rid-off the scripts in a near future 426 private static void patchScripts(ExecutableImage img, List<String> args) throws IOException { 427 Objects.requireNonNull(args); 428 if (!args.isEmpty()) { 429 Files.find(img.getHome().resolve("bin"), 2, (path, attrs) -> { 430 return img.getModules().contains(path.getFileName().toString()); 431 }).forEach((p) -> { 432 try { 433 String pattern = "JLINK_VM_OPTIONS="; 434 byte[] content = Files.readAllBytes(p); 435 String str = new String(content, StandardCharsets.UTF_8); 436 int index = str.indexOf(pattern); 437 StringBuilder builder = new StringBuilder(); 438 if (index != -1) { 439 builder.append(str.substring(0, index)). 440 append(pattern); 441 for (String s : args) { 442 builder.append(s).append(" "); 443 } 444 String remain = str.substring(index + pattern.length()); 445 builder.append(remain); 446 str = builder.toString(); 447 try (BufferedWriter writer = Files.newBufferedWriter(p, 448 StandardCharsets.ISO_8859_1, 449 StandardOpenOption.WRITE)) { 450 writer.write(str); 451 } 452 } 453 } catch (IOException ex) { 454 throw new RuntimeException(ex); 455 } 456 }); 457 } 458 } 459 460 private static String getJavaProcessName() { 461 return isWindows() ? "java.exe" : "java"; 462 } 463 464 public static ExecutableImage getExecutableImage(Path root) { 465 if (Files.exists(root.resolve("bin").resolve(getJavaProcessName()))) { 466 return new DefaultExecutableImage(root, retrieveModules(root)); 467 } 468 return null; 469 } 470 471 private static Set<String> retrieveModules(Path root) { 472 Path releaseFile = root.resolve("release"); 473 Set<String> modules = new HashSet<>(); 474 if (Files.exists(releaseFile)) { 475 Properties release = new Properties(); 476 try (FileInputStream fi = new FileInputStream(releaseFile.toFile())) { 477 release.load(fi); 478 } catch (IOException ex) { 479 System.err.println("Can't read release file " + ex); 480 } 481 String mods = release.getProperty("MODULES"); 482 if (mods != null) { 483 String[] arr = mods.split(","); 484 for (String m : arr) { 485 modules.add(m.trim()); 486 } 487 488 } 489 } 490 return modules; 491 } 492 }