All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.reactiverse.es4x.commands.Install Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 Red Hat, Inc.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *  The Eclipse Public License is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  The Apache License v2.0 is available at
 *  http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */
package io.reactiverse.es4x.commands;

import io.reactiverse.es4x.cli.CmdLineParser;
import io.reactiverse.es4x.asm.FutureBaseVisitor;
import io.reactiverse.es4x.asm.JsonArrayVisitor;
import io.reactiverse.es4x.asm.JsonObjectVisitor;
import io.reactiverse.es4x.cli.GraalVMVersion;
import org.eclipse.aether.artifact.Artifact;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.jar.*;

import static io.reactiverse.es4x.cli.Helper.*;

public class Install implements Runnable {

  public static final String NAME = "install";
  public static final String SUMMARY = "Installs required jars from maven to 'node_modules'.";

  private final boolean silent;

  enum Environment {
    PROD,
    PRODUCTION,
    DEV,
    DEVELOPMENT,
    ALL
  }

  private static final Properties VERSIONS = new Properties();

  static {
    try (InputStream is = Install.class.getClassLoader().getResourceAsStream("META-INF/es4x-commands/VERSIONS.properties")) {
      if (is == null) {
        fatal("Cannot find 'META-INF/es4x-commands/VERSIONS.properties' on classpath");
      } else {
        VERSIONS.load(is);
      }
    } catch (IOException e) {
      fatal(e.getMessage());
    }
  }

  private boolean link;
  private List vendor;
  private File coreJar;
  private Environment environment = Environment.ALL;

  private File cwd;

  public Install(boolean silent) {
    this.silent = silent;
    this.cwd = new File(".");
  }

  public Install(String[] args) {
    silent = false;
    CmdLineParser parser = new CmdLineParser();
    CmdLineParser.Option helpOption = parser.addBooleanOption('h', "help");
    CmdLineParser.Option vendorOption = parser.addStringOption('v', "vendor");
    CmdLineParser.Option linkOption = parser.addBooleanOption('l', "link");
    CmdLineParser.Option environmentOption = parser.addStringOption('e', "environment");

    try {
      parser.parse(args);
    } catch (CmdLineParser.OptionException e) {
      printUsage();
      System.exit(2);
      return;
    }

    Boolean isHelp = parser.getOptionValue(helpOption, Boolean.FALSE);

    if (isHelp != null && isHelp) {
      printUsage();
      System.exit(0);
      return;
    }

    Boolean link = parser.getOptionValue(linkOption, false);
    if (link != null && link) {
      setLink(true);
    }

    setVendor(parser.getOptionValue(vendorOption));
    setEnvironment(parser.getOptionValue(environmentOption, "all"));
    setCwd(new File("."));
  }

  private void printUsage() {
    System.err.println("Usage: es4x " + NAME + " [OPTIONS] [arg...]");
    System.err.println();
    System.err.println(SUMMARY);
    System.err.println();
    System.err.println("Options and Arguments:");
    System.err.println(" -e,--environment\t\t\t\tEnvironment 'prod[uction]/dev[elopment]/all' (default: all).");
    System.err.println(" -l,--link\t\t\t\tSymlink jars instead of copy.");
    System.err.println(" -v,--vendor \tComma separated list of vendor jars.");
    System.err.println();
  }

  public Install setCwd(File cwd) {
    this.cwd = cwd;
    return this;
  }

  public void setLink(boolean link) {
    this.link = link;
  }

  public void setVendor(String vendor) {
    if (vendor != null) {
      this.vendor = new ArrayList<>();
      for (String v : vendor.split(",")) {
        // the jar lives in $dest/.bin (rebase to the project root)
        this.vendor.add(".." + File.separator + ".." + File.separator + v);
      }
    }
  }

  public void setEnvironment(String environment) {
    if (environment != null) {
      this.environment = Environment.valueOf(environment.toUpperCase());
    }
  }

  private void processJson(File json, Set dependencies) throws IOException {
    if (json.exists()) {
      JSONObject npm = JSON.parseObject(json);
      if (npm.has("maven")) {
        final JSONObject maven = npm.getJSONObject("maven");
        // add this dependency
        dependencies.add(maven.get("groupId") + ":" + maven.get("artifactId") + ":" + maven.get("version"));
      }

      switch (environment) {
        case ALL:
        case PROD:
        case PRODUCTION:
          if (npm.has("mvnDependencies")) {
            final JSONArray maven = npm.getJSONArray("mvnDependencies");
            for (Object el : maven) {
              // add this dependency
              dependencies.add((String) el);
            }
          }
      }

      switch (environment) {
        case ALL:
        case DEV:
        case DEVELOPMENT:
          if (npm.has("mvnDevDependencies")) {
            final JSONArray maven = npm.getJSONArray("mvnDevDependencies");
            for (Object el : maven) {
              // add this dependency
              dependencies.add((String) el);
            }
          }
      }
    }
  }

  private void processModules(File dir, Set dependencies) throws IOException {
    File[] mods = dir.listFiles(File::isDirectory);
    if (mods != null) {
      for (File mod : mods) {
        String name = mod.getName();
        if (name.charAt(0) == '@') {
          processModules(mod, dependencies);
          continue;
        }

        File json = new File(mod, "package.json");

        // process
        processJson(json, dependencies);

        File submod = new File(mod, "node_modules");
        if (submod.exists() && submod.isDirectory()) {
          processModules(submod, dependencies);
        }
      }
    }
  }

  private File baseDir;
  private File binDir;
  private boolean hasImportMap;
  private boolean hasPackageJson;

  @Override
  public void run() {

    // mode is dependent on the availability of "package.json"
    if (new File(cwd, "package.json").exists()) {
      baseDir = new File(cwd, "node_modules");
      binDir = new File(baseDir, ".bin");
      hasPackageJson = true;
    } else {
      baseDir = cwd;
      binDir = new File(baseDir, "bin");
    }

    hasImportMap = new File(cwd, "import-map.json").exists();

    final List artifacts = new ArrayList<>();

    final Runnable runAction = () -> {
      if (hasPackageJson) {
        installModules(artifacts, "package.json");
      }
      if (hasImportMap) {
        installModules(artifacts, "import-map.json");
      }
      if (!hasPackageJson && !hasImportMap) {
        installModules(artifacts, null);
      }

      if (!GraalVMVersion.isGraalVM()) {
        final double version = Double.parseDouble(System.getProperty("java.specification.version"));
        if (version >= 11) {
          // verify if the current JDK contains the jdk.internal.vm.ci module
          try {
            String modules = exec(javaHomePrefix() + "java", "--list-modules");
            if (modules.contains("jdk.internal.vm.ci")) {
              warn("Installing JVMCI Compiler...");
              // jvmci compiler + dependencies
              installGraalJMVCICompiler();
            }
          } catch (IOException | InterruptedException e) {
            err(e.getMessage());
          }
        } else {
          warn("Current JDK only supports GraalJS in Interpreted mode!");
        }
        // not on graal, install graaljs and dependencies
        warn("Installing GraalJS...");
        // graaljs + dependencies
        installGraalJS(artifacts);
      }

      // always create a launcher even if no dependencies are needed
      createLauncher(artifacts);
    };

    // control
    if (!hasPackageJson && !hasImportMap) {
      if (!silent) {
        fatal("Nothing to install, missing 'package.json' or 'import-map.json'");
      }
    }

    switch (environment) {
      case ALL:
      case DEV:
      case DEVELOPMENT:
        File control = new File(baseDir, "es4x-launcher.jar");
        if (control.exists()) {
          warn("Skipping install (recent successful run)");
          return;
        }
        break;
    }

    runAction.run();
  }

  private static  void addIfMissing(Collection collection, T element) {
    if (!collection.contains(element)) {
      collection.add(element);
    }
  }

  private void installGraalJS(Collection artifacts) {
    File lib = new File(baseDir, ".lib");
    if (!lib.exists()) {
      if (!lib.mkdirs()) {
        fatal(String.format("Failed to mkdirs '%s'.", lib));
      }
    }

    File jvmci = new File(baseDir, ".jvmci");

    try {
      Resolver resolver = new Resolver();
      List mvnArtifacts = environment == Environment.ALL || environment == Environment.DEV ?
        Collections.singletonList("org.graalvm.tools:chromeinspector:" + VERSIONS.getProperty("graalvm")) :
        Collections.emptyList();

      for (Artifact a : resolver.resolve("org.graalvm.js:js:" + VERSIONS.getProperty("graalvm"), mvnArtifacts)) {
        File destination = new File(lib, a.getFile().getName());
        if (!destination.exists()) {
          // if jvmci is installed we refer to the common dependency
          if (jvmci.exists()) {
            if (new File(jvmci, a.getArtifactId() + "." + a.getExtension()).exists()) {
              addIfMissing(artifacts, ".." + File.separator + ".jvmci" + File.separator + a.getArtifactId() + "." + a.getExtension());
              continue;
            }
          }
          if (link) {
            Files.createSymbolicLink(destination.toPath(), a.getFile().toPath());
          } else {
            Files.copy(a.getFile().toPath(), destination.toPath());
          }
        }
        addIfMissing(artifacts, ".." + File.separator + ".lib" + File.separator +  a.getFile().getName());
      }
    } catch (IOException e) {
      fatal(e.getMessage());
    }
  }

  private void installGraalJMVCICompiler() {
    File jvmci = new File(baseDir, ".jvmci");
    if (!jvmci.exists()) {
      if (!jvmci.mkdirs()) {
        fatal(String.format("Failed to mkdirs '%s'.", jvmci));
      }
    }

    try {
      Resolver resolver = new Resolver();

      for (Artifact a : resolver.resolve("org.graalvm.compiler:compiler:" + VERSIONS.getProperty("graalvm"), Collections.emptyList())) {
        File destination = new File(jvmci, a.getArtifactId() + "." + a.getExtension());
        if (!destination.exists()) {
          if (link) {
            Files.createSymbolicLink(destination.toPath(), a.getFile().toPath());
          } else {
            Files.copy(a.getFile().toPath(), destination.toPath());
          }
        }
      }
    } catch (IOException e) {
      fatal(e.getMessage());
    }
  }

  private void installModules(Collection artifacts, String jsonProject) {
    final Set dependencies = new HashSet<>();

    // process mvnDependencies from CWD package.json
    if (jsonProject != null) {
      try {
        processJson(new File(cwd, jsonProject), dependencies);
      } catch (IOException e) {
        fatal(e.getMessage());
      }

      // crawl node modules
      if (!baseDir.equals(cwd)) {
        if (baseDir.exists() && baseDir.isDirectory()) {
          try {
            processModules(baseDir, dependencies);
          } catch (IOException e) {
            fatal(e.getMessage());
          }
        }
      }
    }

    File libs = new File(baseDir, ".lib");

    try {
      Resolver resolver = new Resolver();

      // lookup root
      String root = "io.reactiverse:es4x:" + VERSIONS.getProperty("es4x");

      for (String el : dependencies) {
        // ensure we respect the wish of the user
        if (el.startsWith("io.reactiverse:es4x:")) {
          root = el;
          break;
        }
      }

      for (Artifact a : resolver.resolve(root, dependencies)) {
        if (!libs.exists()) {
          if (!libs.mkdirs()) {
            fatal(String.format("Failed to mkdirs '%s'.", libs));
          }
        }
        addIfMissing(artifacts, ".." + File.separator + ".lib" + File.separator + a.getFile().getName());
        File destination = new File(libs, a.getFile().getName());

        // locate core jar
        if ("io.vertx".equals(a.getGroupId()) && "vertx-core".equals(a.getArtifactId())) {
          coreJar = a.getFile();
        }

        if (!destination.exists()) {
          if (link) {
            Files.createSymbolicLink(destination.toPath(), a.getFile().toPath());
          } else {
            Files.copy(a.getFile().toPath(), destination.toPath());
          }
        }
      }
    } catch (IOException e) {
      fatal(e.getMessage());
    }
  }

  private void createLauncher(Collection artifacts) {

    // default main script
    String main = null;
    String verticleFactory = "js";

    if (hasPackageJson) {
      try {
        JSONObject npm = JSON.parseObject(new File(cwd, "package.json"));

        // if package json declares a different main, then it shall be used
        if (npm.has("main")) {
          main = (String) npm.get("main");
          // allow main to be a mjs
          if (main != null && main.endsWith(".mjs")) {
            verticleFactory = "mjs";
          }
        }

        // if package json declares a different type, then it shall be used
        if (npm.has("type")) {
          switch ((String) npm.get("type")) {
            case "commonjs":
              verticleFactory = "js";
              break;
            case "module":
              verticleFactory = "mjs";
              break;
            default:
              fatal("Unknown package.json type: " + npm.get("type"));
          }
        }
      } catch (IOException e) {
        fatal(e.getMessage());
      }
    }

    if (!binDir.exists()) {
      if (!binDir.mkdirs()) {
        fatal(String.format("Failed to mkdirs '%s'.", binDir));
      }
    }

    final Manifest manifest = new Manifest();
    final Attributes attributes = manifest.getMainAttributes();

    attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
    attributes.put(new Attributes.Name("Created-By"), "ES4X " + VERSIONS.getProperty("es4x"));
    attributes.put(new Attributes.Name("Built-By"), System.getProperty("user.name"));
    attributes.put(new Attributes.Name("Build-Jdk"), System.getProperty("java.version"));
    attributes.put(Attributes.Name.MAIN_CLASS, "io.reactiverse.es4x.ES4X");
    String classpath = String.join(" ", artifacts);
    if (vendor != null && vendor.size() > 0) {
      classpath += " " + String.join(" ", vendor);
    }
    attributes.put(Attributes.Name.CLASS_PATH, classpath);
    if (main != null) {
      attributes.put(new Attributes.Name("Main-Command"), "run");
      attributes.put(new Attributes.Name("Main-Verticle"), main);
    }
    attributes.put(new Attributes.Name("Default-Verticle-Factory"), verticleFactory);
    if (hasImportMap) {
      attributes.put(new Attributes.Name("Import-Map"), "import-map.json");
    }

    try {
      try (OutputStream os = new FileOutputStream(new File(binDir, "es4x-launcher.jar"))) {
        try (JarOutputStream target = new JarOutputStream(os, manifest)) {
          if (coreJar != null) {
            try (InputStream in = new FileInputStream(coreJar)) {
              try (JarInputStream jar = new JarInputStream(in)) {
                JarEntry je;
                while ((je = jar.getNextJarEntry()) != null) {
                  switch (je.getName()) {
                    case "io/vertx/core/json/JsonObject.class":
                      target.putNextEntry(je);
                      target.write(new JsonObjectVisitor().rewrite(jar));
                      target.closeEntry();
                      break;
                    case "io/vertx/core/json/JsonArray.class":
                      target.putNextEntry(je);
                      target.write(new JsonArrayVisitor().rewrite(jar));
                      target.closeEntry();
                      break;
                    case "io/vertx/core/impl/future/FutureBase.class":
                      target.putNextEntry(je);
                      target.write(new FutureBaseVisitor().rewrite(jar));
                      target.closeEntry();
                      break;
                  }
                }
              } catch (RuntimeException e) {
                warn(e.getMessage());
              }
            } catch (RuntimeException e) {
              warn(e.getMessage());
            }
          }
        }
      }

      // create the launcher scripts
      if (isUnix()) {
        createUNIXScript(binDir);
      }
      if (isWindows()) {
        createDOSScript(binDir);
      }
    } catch (IOException e) {
      fatal(e.getMessage());
    }
  }

  private void createUNIXScript(File bin) throws IOException {

    String relPath = baseDir.equals(cwd) ? ".." : "../..";

    String script =
      "#!/usr/bin/env bash\n" +
        "(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix\n" +
        "\n" +
        "# fight simlinks and avoid readlink -f which doesn't exist on Darwin and Solaris\n" +
        "pushd . > /dev/null\n" +
        "basedir=\"${BASH_SOURCE[0]}\";\n" +
        "while([ -h \"${basedir}\" ]); do\n" +
        "    cd \"`dirname \"${basedir}\"`\"\n" +
        "    basedir=\"$(readlink \"`basename \"${basedir}\"`\")\";\n" +
        "done\n" +
        "cd \"`dirname \"${basedir}\"`\" > /dev/null\n" +
        "basedir=\"`pwd`\";\n" +
        "popd  > /dev/null\n" +
        "\n" +
        "case `uname` in\n" +
        "    *CYGWIN*) basedir=`cygpath -w \"$basedir\"`;;\n" +
        "esac\n" +
        "\n" +
        "if ! [[ -t 1 ]]; then\n" +
        "  TTY_OPTS=-DnoTTY=true\n" +
        "fi\n" +
        "\n" +
        "JAVA_EXE=\"$JAVA_HOME/bin/java\"\n" +
        "if ! [[ -x \"$JAVA_EXE\" ]]; then\n" +
        "  JAVA_EXE=java\n" +
        "fi\n" +
        "\n" +
        "if [[ -d \"$basedir/../.jvmci\" ]]; then\n" +
        "  JVMCI=\"--module-path=$basedir/../.jvmci -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --upgrade-module-path=$basedir/../.jvmci/compiler.jar\"\n" +
        "fi\n" +
        "\n" +
        "if [[ -f \"$basedir/" + relPath + "/security.policy\" ]]; then\n" +
        "  SECURITY_MANAGER=\"-Djava.security.manager -Djava.security.policy=$basedir/" + relPath + "/security.policy\"\n" +
        "fi\n" +
        "\n" +
        "if [[ -f \"$basedir/" + relPath + "/logging.properties\" ]]; then\n" +
        "  LOGGING_PROPERTIES=\"-Djava.util.logging.config.file=$basedir/" + relPath + "/logging.properties\"\n" +
        "fi\n" +
        "\n" +
        "exec \"$JAVA_EXE\" -XX:+IgnoreUnrecognizedVMOptions $JVMCI $SECURITY_MANAGER $LOGGING_PROPERTIES $JAVA_OPTS $TTY_OPTS -jar \"$basedir/es4x-launcher.jar\" \"$@\"\n";

    final File exe = new File(bin, "es4x-launcher");
    try (FileOutputStream out = new FileOutputStream(exe)) {
      out.write(script.getBytes(StandardCharsets.UTF_8));
    }

    // this is a best effort
    if (!exe.setExecutable(true, false)) {
      fatal(String.format("Cannot set script '%s' executable!", exe));
    }
  }

  private void createDOSScript(File bin) throws IOException {

    String relPath = baseDir.equals(cwd) ? ".." : "..\\..";


    String script =
      "@ECHO OFF\n" +
        "\n" +
        "SETLOCAL\n" +
        "\n" +
        "SET \"JAVA_EXE=%JAVA_HOME%\\bin\\java.exe\"\n" +
        "IF NOT EXIST \"%JAVA_EXE%\" (\n" +
        "  SET \"JAVA_EXE=java\"\n" +
        ")\n" +
        "\n" +
        "IF EXIST \"%~dp0\\..\\.jvmci\" (\n" +
        "  SET \"JVMCI=--module-path=\"\"%~dp0\\..\\.jvmci\"\" -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --upgrade-module-path=\"\"%~dp0\\..\\.jvmci\\compiler.jar\"\"\"\n" +
        ")\n" +
        "\n" +
        "IF EXIST \"%~dp0\\" + relPath + "\\security.policy\" (\n" +
        "  SET \"SECURITY_MANAGER=-Djava.security.manager -Djava.security.policy=\"\"%~dp0\\" + relPath + "\\security.policy\"\"\"\n" +
        ")\n" +
        "\n" +
        "IF EXIST \"%~dp0\\" + relPath + "\\logging.properties\" (\n" +
        "  SET \"LOGGING_PROPERTIES=-Djava.util.logging.config.file=\"\"%~dp0\\" + relPath + "\\logging.properties\"\"\"\n" +
        ")\n" +
        "\n" +
        "\"%JAVA_EXE%\" -XX:+IgnoreUnrecognizedVMOptions %JVMCI% %SECURITY_MANAGER% %LOGGING_PROPERTIES% %JAVA_OPTS% -jar \"%~dp0\\es4x-launcher.jar\" %*\n";

    final File exe = new File(bin, "es4x-launcher.cmd");
    try (FileOutputStream out = new FileOutputStream(exe)) {
      out.write(script.getBytes(StandardCharsets.UTF_8));
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy