Package com.redhat.ceylon.compiler.js

Source Code of com.redhat.ceylon.compiler.js.CeylonRunJsTool

package com.redhat.ceylon.compiler.js;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import com.redhat.ceylon.cmr.api.ArtifactContext;
import com.redhat.ceylon.cmr.api.ArtifactResult;
import com.redhat.ceylon.cmr.api.ModuleQuery;
import com.redhat.ceylon.cmr.api.Repository;
import com.redhat.ceylon.cmr.api.RepositoryManager;
import com.redhat.ceylon.cmr.ceylon.RepoUsingTool;
import com.redhat.ceylon.common.Constants;
import com.redhat.ceylon.common.ModuleUtil;
import com.redhat.ceylon.common.Versions;
import com.redhat.ceylon.common.config.DefaultToolOptions;
import com.redhat.ceylon.common.tool.Argument;
import com.redhat.ceylon.common.tool.Description;
import com.redhat.ceylon.common.tool.Option;
import com.redhat.ceylon.common.tool.OptionArgument;
import com.redhat.ceylon.common.tool.RemainingSections;
import com.redhat.ceylon.common.tool.Rest;
import com.redhat.ceylon.common.tool.Summary;
import com.redhat.ceylon.compiler.loader.JsModuleManager;

@Summary("Executes a Ceylon program")
@Description(
        "Executes the ceylon program specified as the `<module>` argument. " +
        "The `<module>` may optionally include a version."
)
@RemainingSections(
        "## Configuration file" +
        "\n\n" +
        "The run-js tool accepts the following option from the Ceylon configuration file: " +
        "`runtool.compile` " +
        "(the equivalent option on the command line always has precedence)." +
        "\n\n" +
        "## EXAMPLE" +
        "\n\n" +
        "The following would execute the `com.example.foobar` module:" +
         "\n\n" +
         "    ceylon run-js com.example.foobar/1.0.0"
)
public class CeylonRunJsTool extends RepoUsingTool {

    /** A thread dedicated to reading from a stream and storing the result to return it as a String. */
    public static class ReadStream extends Thread {
        protected final InputStream in;
        protected final PrintStream out;
        protected final byte[] buf  = new byte[16384];
        public ReadStream(InputStream from, PrintStream to) {
            this.in = from;
            this.out = to;
        }
        public void run() {
            try {
                int count = in.read(buf);
                while (count > 0) {
                    out.write(buf, 0, count);
                    count = in.read(buf);
                }
            } catch (IOException ex) {
                ex.printStackTrace(out);
            }
        }
    }

    /** A thread dedicated to reading from a stream and storing the result to return it as a String. */
    public static class ReadErrorStream extends Thread {
        protected final BufferedReader in;
        protected final PrintStream out;
        protected final byte[] buf  = new byte[16384];
        protected boolean printing = true;
        protected boolean debug = false;
        public ReadErrorStream(InputStream from, PrintStream to, boolean debug) {
            this.in = new BufferedReader(new InputStreamReader(from));
            this.out = to;
            this.debug = debug;
        }
        public void run() {
            try {
                String line = in.readLine();
                while (line != null) {
                    if (line.trim().startsWith("throw ")) {
                        printing = false || debug;
                    } else if (line.startsWith("Error: Cannot find module ")) {
                        out.println(line);
                        printing = false || debug;
                    } else if (!printing) {
                        printing = !(line.isEmpty() || line.startsWith("    at ")) || debug;
                    }
                    if (printing) {
                        out.println(line);
                    }
                    line = in.readLine();
                }
            } catch (IOException ex) {
                ex.printStackTrace(out);
            }
        }
    }

    private static String findNodeInPath(String path) {
        if (path != null) {
            String [] paths = path.split(File.pathSeparator);
            for (String p : paths) {
                String d = p.endsWith(File.separator) ? p : p + File.separator;
                String np = d + "node";
                if (isExe(np)) {
                    return np;
                }
                np = d + "node.exe";
                if (isExe(np)) {
                    return np;
                }
                np = d + "nodejs";
                if (isExe(np)) {
                    return np;
                }
                np = d + "nodejs.exe";
                if (isExe(np)) {
                    return np;
                }
            }
        }
        return null;
    }

    /** Finds the full path to the node.js executable. */
    public static String findNode() {
        String path = getNodeExe();
        if (path != null && !path.isEmpty() && isExe(path)) {
            return path;
        }
        //quick search for most common cases
        String[] paths = { "/usr/bin/node", "/usr/bin/nodejs", "/usr/local/bin/node", "/bin/node", "/opt/bin/node",
                "C:\\Program Files\\nodejs\\node.exe", "C:\\Program Files (x86)\\nodejs\\node.exe",
                "C:\\Program Files\\nodejs\\nodejs.exe", "C:\\Program Files (x86)\\nodejs\\nodejs.exe" };
        for (String p : paths) {
            if (isExe(p)) {
                return p;
            }
        }
        //Now let's look for the executable in all path elements
        String winpath = findNodeInPath(System.getenv("Path"));
        //And why not, look for it in PATH
        if (winpath != null) {
            return winpath;
        }
        winpath = findNodeInPath(System.getenv("PATH"));
        if (winpath != null) {
            return winpath;
        }
        String errmsg = "Could not find 'node' executable. Please install node.js (from http://nodejs.org)."
                + "\nMake sure the path to the node executable is included in your PATH environment variable."
                + "\nIf you have node installed in a non-standard location, you can either set the environment variable"
                + "\nNODE_EXE or the JVM system property node.exe with the full path to the node executable.";
        throw new CeylonRunJsException(errmsg);
    }

    private static boolean isExe(String p) {
        File f = new File(p);
        return f.exists() && f.canExecute();
    }

    private String func = "run";
    private String compileFlags;
    private String module;
    private String exepath;
    private List<String> args;
    private PrintStream output;
    private boolean debug;
    private boolean throwOnError;
   
    public CeylonRunJsTool() {
        super(CeylonRunJsMessages.RESOURCE_BUNDLE);
    }

    /**
     * Tell the tool not to exit on a non-zero exit code from node, but throw otherwise. This is not
     * used by the command-line, but can be useful when invoked via the API.
     * @param throwOnError true to throw instead of calling System.exit. Defaults to false.
     */
    public void setThrowOnError(boolean throwOnError) {
        this.throwOnError = throwOnError;
    }
   
    /**
     * Check if we throw on a non-zero exit code from node, rather than exit. This is not
     * used by the command-line, but can be useful when invoked via the API.
     * @return true to throw instead of calling System.exit. Defaults to false.
     */
    public boolean isThrowOnError() {
        return throwOnError;
    }
   
    /** Sets the PrintStream to use for output. Default is System.out. */
    public void setOutput(PrintStream value) {
        output = value;
    }

    @OptionArgument(argumentName="debug")
    @Description("Shows more detailed output in case of errors.")
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    @OptionArgument(argumentName="func")
    @Description("The function to run, which must be exported from the " +
        "given `<module>`. (default: `run`).")
    public void setRun(String func) {
        this.func = func;
    }

    @Option
    @OptionArgument(argumentName = "flags")
    @Description("Determines if and how compilation should be handled. " +
            "Allowed flags include: `never`, `once`, `force`, `check`.")
    public void setCompile(String compile) {
        this.compileFlags = compile;
    }

    @Argument(argumentName="module", multiplicity="1", order=1)
    public void setModuleVersion(String moduleVersion) {
        this.module= moduleVersion;
    }

    @Rest
    public void setArgs(List<String> args) {
        this.args = args;
    }

    @OptionArgument(argumentName="node-exe")
    @Description("The path to the node.js executable. Will be searched in standard locations if not specified.")
    public void setNodeExe(String path) {
        this.exepath=path;
    }

    private static String getNodePath() {
        return getFromEnv("NODE_PATH", "node.path");
    }

    private static String getNodeExe() {
        return getFromEnv("NODE_EXE", "node.exe");
    }

    private static String getCeylonRepo() {
        return getFromEnv("CEYLON_REPO", Constants.PROP_CEYLON_SYSTEM_REPO);
    }

    private static String getFromEnv(String env, String prop){
        String path = System.getenv(env);
        if (path != null) {
            return path;
        }
        return System.getProperty(prop);
    }

    /** Creates a ProcessBuilder ready to run the node.js executable with the specified parameters.
     * @param module The module name and version (if it's not the default).
     * @param func The function name to run (must be specified)
     * @param args The optional command-line arguments to pass to the function
     * @param exepath The full path to the node.js executable
     * @param repos The list of repository paths (used as module paths for node.js)
     * @param output An optional PrintStream to write the output of the node.js process to. */
    private ProcessBuilder buildProcess(final String module, final String version, String func, List<String> args,
            String exepath, final List<File> repos, PrintStream output) {
        final String node = exepath == null ? findNode() : exepath;
        if (exepath != null) {
            File _f = new File(exepath);
            if (!(_f.exists() && _f.canExecute())) {
                throw new CeylonRunJsException("Specified node.js executable is invalid.");
            }
        }

        //Rename func
        if (func.startsWith("::")) {
            func = func.substring(2);
        } else if (func.indexOf('.') > 0 || func.indexOf("::") > 0) {
            //Given a fully qualified name such as a.b.c.run, remove the module path first
            //then change what remains to run$subpackages i.e. module a.b then run$c
            if (func.contains("::")) {
                func = func.replace("::", ".");
            }
            if (func.startsWith(module)) {
                func = func.substring(module.length()+1);
            }
            if (func.indexOf('.') > 0) {
                final StringBuilder fsb = new StringBuilder();
                final int lastDot = func.lastIndexOf('.');
                fsb.append(func.substring(lastDot+1)).append('$');
                fsb.append(func.substring(0,lastDot).replaceAll("\\.", "\\$"));
                func = fsb.toString();
            }
        }
        if (JsIdentifierNames.isReservedWord(func)) {
            func = "$_" + func;
        }
        final boolean isDefault = ModuleUtil.isDefaultModule(module);
        String moduleString = isDefault ? module : module +"/"+version;
        //The timeout is to have enough time to start reading on the process streams
       
        String eval = String.format("if(typeof setTimeout==='function'){setTimeout(function(){},50)};" +
                "var __entry_point__=require('%s%s/%s%s').%s;if (__entry_point__===undefined){" +
                "console.log('The specified method \"%s\" does not exist or is not shared in the %s module');" +
                "process.exit(1);}else __entry_point__();",
                module.replace(".", "/"),
                isDefault ? "" : "/" + version,
                module,
                isDefault ? "" : "-" + version,
                func, func, moduleString);
        final ProcessBuilder versionProc = new ProcessBuilder(java.util.Arrays.asList(node, "-v"));
        try {
            Process versionProcess = versionProc.start();
            BufferedReader lineReader = new BufferedReader(new InputStreamReader(versionProcess.getInputStream()));
            String nodeVersion = lineReader.readLine();
            versionProcess.destroy();
            if (nodeVersion.charAt(0)=='v') {
                nodeVersion = nodeVersion.substring(1);
            }
            String[] versionParts = nodeVersion.split("\\.");
            if (versionParts.length > 1) {
                if (Integer.parseInt(versionParts[0], 10) == 0 && Integer.parseInt(versionParts[1], 10) < 8) {
                    System.out.println("Be warned, old timer: JavaScript code generated by the Ceylon compiler will most likely not run on node.js versions older than 0.8");
                    System.out.println();
                }
            }
        } catch (IOException|NumberFormatException ex) {
            System.out.println("Cannot determine node.js version; you should be using 0.8 or above");
        }
        final ProcessBuilder proc;
        if (args != null && !args.isEmpty()) {
            args.add(0, node);
            args.add(1, "-e");
            args.add(2, eval);
            args.add(3, "dummy"); // See https://github.com/ceylon/ceylon.language/issues/503
            proc = new ProcessBuilder(args.toArray(new String[0]));
        } else {
            proc = new ProcessBuilder(node, "-e", eval);
        }
        StringBuilder nodePath = new StringBuilder();
        appendToNodePath(nodePath, getNodePath());
        //Now append repositories
        for (File repo : repos) {
            appendToNodePath(nodePath, repo.getPath());
        }
        if (debug) {
            System.out.println("NODE_PATH=" + nodePath);
        }
        proc.environment().put("NODE_PATH", nodePath.toString());
        if (output != null) {
            proc.redirectErrorStream();
        }
        return proc;
    }

    private static String appendToNodePath(StringBuilder nodePath, String repo) {
        if (repo == null || repo.isEmpty()) return "";
        if (nodePath.length() > 0) {
            nodePath.append(File.pathSeparator);
        }
        nodePath.append(repo);
        return repo;
    }

    private List<Object> getDependencies(File jsmod) throws IOException {
        final Map<String,Object> model = JsModuleManager.loadJsonModel(jsmod);
        if (model == null) {
            return Collections.emptyList();
        }
        @SuppressWarnings("unchecked")
        List<Object> deps = (List<Object>)model.get("$mod-deps");
        return deps;
    }

    protected void loadDependencies(List<File> repos, RepositoryManager repoman, File jsmod) throws IOException {
        final List<Object> deps = getDependencies(jsmod);
        if (deps == null) {
            return;
        }
        for (Object dep : deps) {
            final String depname;
            boolean optional = false;
            if (dep instanceof String) {
                //it's a mandatory dependency
                depname = (String)dep;
            } else {
                @SuppressWarnings("unchecked")
                final Map<String,Object> depmap = (Map<String,Object>)dep;
                depname = depmap.get("path").toString();
                optional = new Integer(1).equals(depmap.get("opt"));
            }
            //Module names have escaped forward slashes due to JSON encoding
            int idx = depname.indexOf('/');
            final String modname = depname.substring(0, idx);
            final String modvers = depname.substring(idx+1);
            File other = getArtifact(repoman, modname, modvers, optional);
            if (other != null) {
                final File f = getRepoDir(modname, other);
                if (!repos.contains(f)) {
                    repos.add(f);
                }
                loadDependencies(repos, repoman, other);
            }
        }
    }

    @Override
    public void initialize() {
    }

    @Override
    public void run() throws Exception {
        setSystemProperties();
        //The timeout is to have enough time to start reading on the process streams
        if (systemRepo == null) {
            systemRepo = getCeylonRepo();
        }
        final boolean isDefault = ModuleUtil.isDefaultModule(module);
        String version;
        final String modname;
        if (isDefault) {
            modname = module;
            version = "";
        } else {
            version = ModuleUtil.moduleVersion(module);
            modname = ModuleUtil.moduleName(module);
        }

        if (compileFlags == null) {
            compileFlags = DefaultToolOptions.getRunToolCompileFlags();
            if (compileFlags.isEmpty()) {
                compileFlags = COMPILE_NEVER;
            }
        } else if (compileFlags.isEmpty()) {
            compileFlags = COMPILE_ONCE;
        }
       
        //Create a repository manager to load the js module we're going to run
        final RepositoryManager repoman = getRepositoryManager();
       
        version = checkModuleVersionsOrShowSuggestions(
                repoman, modname, version, ModuleQuery.Type.JS,
                Versions.JS_BINARY_MAJOR_VERSION, Versions.JS_BINARY_MINOR_VERSION, compileFlags);
        if (version == null) {
            return;
        }
        File jsmod = getArtifact(repoman, modname, version, false);
        // NB localRepos will contain a set of files pointing to the module repositories
        // where all the needed modules can be found
        List<File> localRepos = new ArrayList<>();
        for (Repository r : repoman.getRepositories()) {
            if (!r.getRoot().isRemote()) {
                File f = new File(r.getDisplayString());
                if (!localRepos.contains(f)) {
                    localRepos.add(f);
                }
            }
        }
        File rd = getRepoDir(modname, jsmod);
        if (!localRepos.contains(rd)) {
            localRepos.add(rd);
        }
        loadDependencies(localRepos, repoman, jsmod);
        customizeDependencies(localRepos, repoman);

        final ProcessBuilder proc = buildProcess(modname, version, func, args, exepath, localRepos, output);
        Process nodeProcess = proc.start();
        //All this shit because inheritIO doesn't work on fucking Windows
        new ReadStream(nodeProcess.getInputStream(), output == null ? System.out : output).start();
        if (output == null) {
            new ReadErrorStream(nodeProcess.getErrorStream(), System.err, debug).start();
        }
        int exitCode = nodeProcess.waitFor();
        if (exitCode != 0) {
            if(throwOnError)
                throw new RuntimeException("Node process exited with non-zero exit code: "+exitCode);
            else
                System.exit(exitCode==11?2:exitCode);
        }
    }

    // Make sure JS and JS_MODEL artifacts exist and try to obtain the RESOURCES as well
    protected File getArtifact(RepositoryManager repoman, String modName, String modVersion, boolean optional) {
        ArtifactContext ac = new ArtifactContext(modName, modVersion, ArtifactContext.JS, ArtifactContext.JS_MODEL, ArtifactContext.RESOURCES);
        ac.setIgnoreDependencies(true);
        ac.setThrowErrorIfMissing(false);
        List<ArtifactResult> results = repoman.getArtifactResults(ac);
        ArtifactResult code = getArtifactType(results, ArtifactContext.JS);
        ArtifactResult model = getArtifactType(results, ArtifactContext.JS_MODEL);
        if (code == null || model == null) {
            if (optional) {
                return null;
            }
            throw new CeylonRunJsException("Cannot find module " + ModuleUtil.makeModuleName(modName, modVersion) + " in specified module repositories");
        }
        return model.artifact();
    }

    protected ArtifactResult getArtifactType(List<ArtifactResult> results, String suffix) {
        for (ArtifactResult r : results) {
            String s = ArtifactContext.getSuffixFromFilename(r.artifact().getName());
            if (s.equals(suffix)) {
                return r;
            }
        }
        return null;
    }

    protected File getRepoDir(String modname, File file) {
        // A trippy way to get to the repo folder, but it works
        int count = modname.split("\\.").length + 1;
        if (!ModuleUtil.isDefaultModule(modname)) {
            count++;
        }
        for (int i=0; i < count; i++) {
            file = file.getParentFile();
        }
        return file;
    }

    protected void customizeDependencies(List<File> localRepos, RepositoryManager repoman) throws IOException {
    }

    // use to test and debug:
//    public static void main(String[] args) throws Exception{
//      CeylonRunJsTool tool = new CeylonRunJsTool();
//      tool.setCwd(new File("../ceylon-js-tests"));
//      tool.setModuleVersion("default");
//      tool.run();
//    }
}
TOP

Related Classes of com.redhat.ceylon.compiler.js.CeylonRunJsTool

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.