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