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