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