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