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 }