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