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 if (Files.isDirectory(bin)) { 194 Files.list(bin) 195 .filter(f -> !f.toString().endsWith(".diz")) 196 .filter(f -> Files.isRegularFile(f)) 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 String path = "/" + module + "/module-info.class"; 258 Optional<ResourcePoolEntry> res = imageContent.findEntry(path); 259 if (!res.isPresent()) { 260 throw new IOException("module-info.class not found for " + module + " module"); 261 } 262 Optional<String> mainClass; 263 ByteArrayInputStream stream = new ByteArrayInputStream(res.get().contentBytes()); 264 mainClass = ModuleDescriptor.read(stream).mainClass(); 265 if (mainClass.isPresent()) { 266 Path cmd = root.resolve("bin").resolve(module); 267 // generate shell script for Unix platforms 268 StringBuilder sb = new StringBuilder(); 269 sb.append("#!/bin/sh") 270 .append("\n"); 271 sb.append("JLINK_VM_OPTIONS=") 272 .append("\n"); 273 sb.append("DIR=`dirname $0`") 274 .append("\n"); 275 sb.append("$DIR/java $JLINK_VM_OPTIONS -m ") 276 .append(module).append('/') 277 .append(mainClass.get()) 278 .append(" $@\n"); 279 280 try (BufferedWriter writer = Files.newBufferedWriter(cmd, 281 StandardCharsets.ISO_8859_1, 282 StandardOpenOption.CREATE_NEW)) { 283 writer.write(sb.toString()); 284 } 285 if (Files.getFileStore(root.resolve("bin")) 286 .supportsFileAttributeView(PosixFileAttributeView.class)) { 287 setExecutable(cmd); 288 } 289 // generate .bat file for Windows 290 if (isWindows()) { 291 Path bat = root.resolve("bin").resolve(module + ".bat"); 292 sb = new StringBuilder(); 293 sb.append("@echo off") 294 .append("\r\n"); 295 sb.append("set JLINK_VM_OPTIONS=") 296 .append("\r\n"); 297 sb.append("set DIR=%~dp0") 298 .append("\r\n"); 299 sb.append("\"%DIR%\\java\" %JLINK_VM_OPTIONS% -m ") 300 .append(module).append('/') 301 .append(mainClass.get()) 302 .append(" %*\r\n"); 303 304 try (BufferedWriter writer = Files.newBufferedWriter(bat, 305 StandardCharsets.ISO_8859_1, 306 StandardOpenOption.CREATE_NEW)) { 307 writer.write(sb.toString()); 308 } 309 } 310 } 311 } 312 } 313 314 @Override 315 public DataOutputStream getJImageOutputStream() { 316 try { 317 Path jimageFile = mdir.resolve(BasicImageWriter.MODULES_IMAGE_NAME); 318 OutputStream fos = Files.newOutputStream(jimageFile); 319 BufferedOutputStream bos = new BufferedOutputStream(fos); 320 return new DataOutputStream(bos); 321 } catch (IOException ex) { 322 throw new UncheckedIOException(ex); 323 } 324 } 325 326 private void accept(ResourcePoolEntry file) throws IOException { 327 String fullPath = file.path(); 328 String module = "/" + file.moduleName() + "/"; 329 String filename = fullPath.substring(module.length()); 330 // Remove radical native|config|... 331 filename = filename.substring(filename.indexOf('/') + 1); 332 try (InputStream in = file.content()) { 333 switch (file.type()) { 334 case NATIVE_LIB: 335 writeEntry(in, destFile(nativeDir(filename), filename)); 336 break; 337 case NATIVE_CMD: 338 Path path = destFile("bin", filename); 339 writeEntry(in, path); 340 path.toFile().setExecutable(true); 341 break; 342 case CONFIG: 343 writeEntry(in, destFile("conf", filename)); 344 break; 345 case TOP: 346 break; 347 case OTHER: 348 if (file instanceof SymImageFile) { 349 SymImageFile sym = (SymImageFile) file; 350 Path target = root.resolve(sym.getTargetPath()); 351 if (!Files.exists(target)) { 352 throw new IOException("Sym link target " + target 353 + " doesn't exist"); 354 } 355 writeSymEntry(root.resolve(filename), target); 356 } else { 357 writeEntry(in, root.resolve(filename)); 358 } 359 break; 360 default: 361 throw new InternalError("unexpected entry: " + fullPath); 362 } 363 } 364 } 365 366 private Path destFile(String dir, String filename) { 367 return root.resolve(dir).resolve(filename); 368 } 369 370 private void writeEntry(InputStream in, Path dstFile) throws IOException { 371 Objects.requireNonNull(in); 372 Objects.requireNonNull(dstFile); 373 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 374 Files.copy(in, dstFile); 375 } 376 377 private void writeSymEntry(Path dstFile, Path target) throws IOException { 378 Objects.requireNonNull(dstFile); 379 Objects.requireNonNull(target); 380 Files.createDirectories(Objects.requireNonNull(dstFile.getParent())); 381 Files.createLink(dstFile, target); 382 } 383 384 private String nativeDir(String filename) { 385 if (isWindows()) { 386 if (filename.endsWith(".dll") || filename.endsWith(".diz") 387 || filename.endsWith(".pdb") || filename.endsWith(".map")) { 388 return "bin"; 389 } else { 390 return "lib"; 391 } 392 } else { 393 return "lib"; 394 } 395 } 396 397 private boolean isWindows() { 398 return targetOsName.startsWith("Windows"); 399 } 400 401 /** 402 * chmod ugo+x file 403 */ 404 private void setExecutable(Path file) { 405 try { 406 Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file); 407 perms.add(PosixFilePermission.OWNER_EXECUTE); 408 perms.add(PosixFilePermission.GROUP_EXECUTE); 409 perms.add(PosixFilePermission.OTHERS_EXECUTE); 410 Files.setPosixFilePermissions(file, perms); 411 } catch (IOException ioe) { 412 throw new UncheckedIOException(ioe); 413 } 414 } 415 416 private static void createUtf8File(File file, String content) throws IOException { 417 try (OutputStream fout = new FileOutputStream(file); 418 Writer output = new OutputStreamWriter(fout, "UTF-8")) { 419 output.write(content); 420 } 421 } 422 423 @Override 424 public ExecutableImage getExecutableImage() { 425 return new DefaultExecutableImage(root, modules); 426 } 427 428 // This is experimental, we should get rid-off the scripts in a near future 429 private static void patchScripts(ExecutableImage img, List<String> args) throws IOException { 430 Objects.requireNonNull(args); 431 if (!args.isEmpty()) { 432 Files.find(img.getHome().resolve("bin"), 2, (path, attrs) -> { 433 return img.getModules().contains(path.getFileName().toString()); 434 }).forEach((p) -> { 435 try { 436 String pattern = "JLINK_VM_OPTIONS="; 437 byte[] content = Files.readAllBytes(p); 438 String str = new String(content, StandardCharsets.UTF_8); 439 int index = str.indexOf(pattern); 440 StringBuilder builder = new StringBuilder(); 441 if (index != -1) { 442 builder.append(str.substring(0, index)). 443 append(pattern); 444 for (String s : args) { 445 builder.append(s).append(" "); 446 } 447 String remain = str.substring(index + pattern.length()); 448 builder.append(remain); 449 str = builder.toString(); 450 try (BufferedWriter writer = Files.newBufferedWriter(p, 451 StandardCharsets.ISO_8859_1, 452 StandardOpenOption.WRITE)) { 453 writer.write(str); 454 } 455 } 456 } catch (IOException ex) { 457 throw new RuntimeException(ex); 458 } 459 }); 460 } 461 } 462 463 public static ExecutableImage getExecutableImage(Path root) { 464 Path binDir = root.resolve("bin"); 465 if (Files.exists(binDir.resolve("java")) || 466 Files.exists(binDir.resolve("java.exe"))) { 467 return new DefaultExecutableImage(root, retrieveModules(root)); 468 } 469 return null; 470 } 471 472 private static Set<String> retrieveModules(Path root) { 473 Path releaseFile = root.resolve("release"); 474 Set<String> modules = new HashSet<>(); 475 if (Files.exists(releaseFile)) { 476 Properties release = new Properties(); 477 try (FileInputStream fi = new FileInputStream(releaseFile.toFile())) { 478 release.load(fi); 479 } catch (IOException ex) { 480 System.err.println("Can't read release file " + ex); 481 } 482 String mods = release.getProperty("MODULES"); 483 if (mods != null) { 484 String[] arr = mods.split(","); 485 for (String m : arr) { 486 modules.add(m.trim()); 487 } 488 489 } 490 } 491 return modules; 492 } 493 }