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