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

com.redhat.ceylon.ant.CeylonAntTask Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
/*
 * Copyright Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the authors tag. All rights reserved.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License version 2.
 * 
 * This particular file is subject to the "Classpath" exception as provided in the 
 * LICENSE file that accompanied this code.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License,
 * along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */

package com.redhat.ceylon.ant;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.util.FileUtils;

import com.redhat.ceylon.launcher.CeylonClassLoader;
import com.redhat.ceylon.launcher.ClassLoaderSetupException;
import com.redhat.ceylon.launcher.Launcher;

/**
 * Baseclass for ant tasks which execute a ceylon tool in a subprocess.
 */
public abstract class CeylonAntTask extends Task {

    private static final int MAX_COMMAND_LENGTH = 4096;
    
    @AntDoc("The value for the system property can "
            + "either be passed as a `value` attribute:\n"
            + "\n"
            + "    \n"
            + "\n"
            + "or it can be the text between the begin and end tags:\n"
            + "\n"
            + "\n"
            + "    Hi\n"
            + "\n"
            + "Alternatively, it is "
            + "posible to dispense with the attributes and use the "
            + "syntax\n"
            + "\n"
            + "    org.example.helloworld.greeting=Hi\n")
    public static class Define {
        String key;
        String value;
        
        @AntDoc("The property to be defined")
        public void setKey(String key) {
            this.key = key;
        }

        @AntDoc("The value of the define")
        public void setValue(String value) {
            this.value = value;
        }

        @AntDocIgnore
        public void addText(String value) {
            this.value = value;
        }
    }

    private final String toolName;
    private File executable;
    private ExitHandler exitHandler = new ExitHandler();
    private Boolean fork;
    private Boolean inheritAll;
    private boolean stacktraces;

    private String cwd;
    private String config;
    private String verbose;
    private List defines = new ArrayList(0);
    
    private CeylonClassLoader loader;
    
    /**
     * @param toolName The name of the ceylon tool which this task executes.
     */
    protected CeylonAntTask(String toolName) {
        this.toolName = toolName;
    }
    
    protected CeylonClassLoader getLoader() throws ClassLoaderSetupException {
        if(loader == null){
            loader = Util.getCeylonClassLoaderCachedInProject(getProject());
        }
        return loader;
    }
    
    protected String getCwd() {
        return cwd;
    }

    /**
     * Sets the current working directory
     * @param cwd path to the current working directory
     */
    @OptionEquivalent("--cwd")
    public void setCwd(String cwd) {
        this.cwd = cwd;
    }

    protected String getConfig() {
        return config;
    }

    /**
     * Specifies the configuration file to use for this task
     * @param config path to the configuration file
     */
    @OptionEquivalent("--config")
    public void setConfig(String config) {
        this.config = config;
    }

    @OptionEquivalent("--verbose")
    public void setVerbose(String verbose){
        this.verbose = verbose;
    }

    /** Adds an define (key=value) to be passed to the tool */
    @AntDoc("A `` element is used to set system properties for the ant "
            + "task being executed.")
    @OptionEquivalent("--define")
    public void addConfiguredDefine(Define define) {
        this.defines.add(define);
    }

    public String getExecutable() {
        return executable.getPath();
    }
    
    /** Sets the location of the ceylon executable script */
    @AntDoc("The location of the ceylon executable script.")
    public void setExecutable(String executable) {
        this.executable = new File(Util.getScriptName(executable));
    }
    
    public boolean getFork() {
        return (fork != null) ? fork : shouldSpawnJvm();
    }

    /** Sets whether the task should be run in a separate VM */
    @AntDoc("Whether the task should be run in a separate VM (default: `false`)")
    public void setFork(boolean fork) {
        this.fork = fork;
    }
    
    public boolean getInheritAll() {
        return (inheritAll != null) ? inheritAll : getFork();
    }

    /**
     * Sets whether a task should inherit environment and properties.
     * Only applies when forked == true
     */
    @AntDoc("Whether a task should inherit environment and properties. "
            + "Only applies when `forked == true`.")
    public void setInheritAll(boolean inheritAll) {
        this.inheritAll = inheritAll;
    }
    
    public boolean getStacktraces() {
        return stacktraces;
    }

    /** Sets whether the task should show any stacktraces that are generated during execution */
    @OptionEquivalent("--stacktraces")
    public void setStacktraces(boolean stacktraces) {
        this.stacktraces = stacktraces;
    }
    
    public boolean getFailOnError() {
        return exitHandler.isFailOnError();
    }

    /** Sets whether an error in executing this task should fail the build */
    @AntDoc("Whether an error in executing this task should fail the ant build")
    public void setFailOnError(boolean failOnError) {
        this.exitHandler.setFailOnError(failOnError);
    }
    
    public String getErrorProperty() {
        return exitHandler.getErrorProperty();
    }

    /** Sets name of a property to set to true in the event of an error */
    @AntDoc("The ant property to set to true in the event of an error")
    public void setErrorProperty(String errorProperty) {
        this.exitHandler.setErrorProperty(errorProperty);
    }
    
    /**
     * Sets the Ceylon program exit code into the given property
     * @param resultProperty the property to set to the Ceylon program exit code
     */
    @AntDoc("The ant property to set to the Ceylon program exit code")
    public void setResultProperty(String resultProperty){
        this.exitHandler.setResultProperty(resultProperty);
    }
    
    /** Executes the task */
    public void execute() {
        Java7Checker.check();
        
        checkParameters();
        Commandline cmd = buildCommandline();
        if (cmd != null) {
            executeCommandline(cmd);
        }
    }

    /**
     * Executes a Commandline
     * @param cmd The commandline
     */
    protected void executeCommandline(Commandline cmd) {
        File tmpFile = null;
        try {
            int exitValue;
            if(getFork()){
                log("Spawning new process: " + Arrays.toString(cmd.getCommandline()), Project.MSG_VERBOSE);
                
                String[] args = cmd.getCommandline();
                if (Commandline.toString(args).length() > MAX_COMMAND_LENGTH) {
                    BufferedWriter out = null;
                    try {
                        tmpFile = File.createTempFile("ceylon-ant-", "cmd");
                        out = new BufferedWriter(new FileWriter(tmpFile));
                        for (int i = 1; i < args.length; i++) {
                            out.write(Commandline.quoteArgument(args[i]));
                            out.newLine();
                        }
                        out.flush();
                        String[] newArgs = new String[2];
                        newArgs[0] = args[0];
                        newArgs[1] = "@" + tmpFile.getAbsolutePath();
                        args = newArgs;
                    } finally {
                        FileUtils.close(out);
                    }
                }
                
                Execute exe = new Execute(new LogStreamHandler(this, Project.MSG_INFO, Project.MSG_WARN));
                exe.setAntRun(getProject());
                exe.setWorkingDirectory(getProject().getBaseDir());
                exe.setCommandline(args);
                exe.setNewenvironment(!getInheritAll());
                exe.execute();
                exitValue = exe.getExitValue();
            }else{
                log("Launching Launcher in this JVM: " + Arrays.toString(cmd.getArguments()), Project.MSG_VERBOSE);
                exitValue = Launcher.runInJava7Checked(getLoader(), cmd.getArguments());
            }
            if (exitValue != 0) {
                String message = formatFailureMessage(cmd);
                exitHandler.handleExit(this, exitValue, message);
            }else{
                exitHandler.handleExit(this, exitValue, null);
            }
        }catch(BuildException e){
            // let build exceptions through, since we throw them ourselves in handleExit!
            throw e;
        } catch (Throwable e) {
            throw new BuildException("Error running Ceylon " + toolName + " tool (an exception was thrown, run ant with -v parameter to see the exception)", e, getLocation());
        } finally {
            if (tmpFile != null) {
                tmpFile.delete();
            }
        }
    }

    /**
     * For now this is not a setting, because we must have it for ceylon-run and it's kinda useless for the
     * other tools.
     * See https://github.com/ceylon/ceylon-compiler/issues/1076
     * See https://github.com/ceylon/ceylon-compiler/issues/1366
     */
    protected boolean shouldSpawnJvm() {
        return false;
    }

    protected String formatFailureMessage(Commandline cmd) {
        StringBuilder sb = new StringBuilder("While executing command").append(System.lineSeparator());
        for (String s : cmd.getCommandline()) {
            sb.append("   ").append(s).append(System.lineSeparator());
        }
        sb.append(getFailMessage());
        String message = sb.toString();
        return message;
    }

    /**
     * Returns the message to pass to {@link ExitHandler#handleExit(Task, int, String)}
     */
    protected abstract String getFailMessage();

    /**
     * Builds the command line to be executed
     * @return the command line to be executed, 
     * or null if there is no command to be executed
     */
    protected Commandline buildCommandline() {
        Commandline cmd = new Commandline();
        cmd.setExecutable(findExecutable());
        if (stacktraces) {
            appendOption(cmd, "--stacktraces");
        }
        if (getCwd() != null) {
            appendOption(cmd, "--cwd=" + getCwd());
        } else {
            appendOption(cmd, "--cwd=" + getProject().getBaseDir().getPath());
        }
        if (getConfig() != null) {
            appendOption(cmd, "--config=" + getConfig());
        }
        if (getFork() && getInheritAll()) {
            @SuppressWarnings("rawtypes")
            Hashtable props = getProject().getUserProperties();
            for (Object k : props.keySet()) {
                Object val = props.get(k);
                String arg = k + "=" + val;
                if (isSafeProperty(arg)) {
                    appendOption(cmd, "--define=" + encodeProperty(arg));
                }
            }
        }
        for (Define d : defines) {
            String arg = (d.key != null) ? d.key + "=" + d.value : d.value;
            appendOption(cmd, "--define=" + arg);
        }
        cmd.createArgument().setValue(toolName);
        completeCommandline(cmd);
        return cmd;
    }

    private boolean isSafeProperty(String str) {
        return !str.contains("\"") || !str.contains("'");
    }
    
    private String encodeProperty(String str) {
        return str.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
    }
    
    /**
     * Completes the building of a partially configured Commandline
     * @param cmd
     */
    protected void completeCommandline(Commandline cmd) {
        appendVerboseOption(cmd, verbose);
    }

    /**
     * Checks the parameters
     */
    protected void checkParameters() {
        findExecutable();
    }
    
    /**
     * Tries to find a ceylonc compiler either user-specified or detected
     */
    private String findExecutable() {
        return Util.findCeylonScript(this.executable, getProject());
    }
    
    /**
     * Adds a {@code --verbose} (or {@code --verbose=flags}) to the given 
     * command line
     * @param cmd The command line
     * @param verbose The verbose flags: {@code "true"}, {@code "yes"} or 
     * {@code "on"} just adds a plain {@code --verbose} option, 
     * {@code "false"}, {@code "no"} or 
     * {@code "off"} doesn't add any option, 
     * otherwise the value is 
     * treated as a list of flags and is passed verbatim via {@code --verbose=...}.
     */
    protected static void appendVerboseOption(Commandline cmd, String verbose) {
        if (verbose != null
                && !"false".equals(verbose)
                && !"no".equals(verbose)
                && !"off".equals(verbose)) {
            if ("true".equals(verbose)
                    || "yes".equals(verbose)
                    || "on".equals(verbose)) {
                // backward compat with verbose previously being a Boolean
                appendOption(cmd, "--verbose");
            } else {
                appendOption(cmd, "--verbose="+verbose);
            }
        }
    }
    
    /**
     * Adds a long option argument to the given command line iff the given 
     * value is not null.
     * @param cmd The command line
     * @param longOption The long option name (including the {@code --}, but 
     * excluding the {@code =}).
     * @param value The option value
     */
    protected static void appendOptionArgument(Commandline cmd, String longOption, String value) {
        if (value != null){
            cmd.createArgument().setValue(longOption);
            cmd.createArgument().setValue(value);
        }
    }
    
    /**
     * Adds a long option (with no argument) to the given command line.
     * @param cmd The command line
     * @param longOption The long option name (including the {@code --} 
     */
    protected static void appendOption(Commandline cmd, String longOption) {
        cmd.createArgument().setValue(longOption);
    }
    
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy