Thursday, July 12, 2012

AntForkTask for Ant

Ant already has <ant> and <antcall>, but these two tasks have one particular restriction: They both execute in the same VM as the Ant program executing them. What if I want to run Ant in a separate VM?  Ok, you can accomplish this using the <java> task with fork=true, but that takes a lot more effort to set up each time. It is far from resuable, and in fact is rather frustrating.

I actually had a lot of reasons to run Ant in a separate VM, so I wrote a task to do this. It looks like this:

<target name="doTest">
    <antfork buildfile="test/build-test.xml" target="test" verbose="${verbose}" emacs="yes">
        <classpath>
            <fileset dir="test/lib" includes="**"/>
        </classpath>
        <property name="test.type" value="${test.type}"/>
        <property name="log4j.config" location="${log4j.config}"/>
        <property name="module.build" value="${module.build}"/>
        <property name="module.dir" value="${module.dir}"/>
        <property name="module.compile.classpath" value="${module.compile.classpath}"/>
    </antfork>
</target>

I have found this most useful for adding JUnit to the classpath in order to use the JUnit and JUnitReport tasks. AntFork is also useful in combination with the CallOver task in order to distribute load. One virtual machine may not be up to the task of several parallel targets munching up memory. Parallel targets that use AntFork can allow the separate virtual machines to deal with memory-intensive processes.

In fact, I found the AntForkTask so useful that I was astonished that this functionality was not added to Ant before, and I am completely dumbfounded that over 7 years later that Ant still lacks this capability.

Here is the source code that I used for Ant 1.5. I have no doubt it could use some updating for the latest version of Ant.

/*
  Copyright 2005  Steven S. Morgan

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.


  steven@technicalabilities.com
  http://www.technicalabilities.com/java

 */

package com.technicalabilities.ant;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import java.util.Iterator;
import java.util.Vector;
import java.util.Properties;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.Enumeration;

import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;

import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.taskdefs.Property;

import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Environment;

/**
   <p>
   The AntForkTask allows an ant task to be executed in a new process.
   Memory hogs such as the <tt>&lt;style&gt;</tt> task can be executed
   separately and the memory recovered.  Computer processing power can
   be used more effectively.  Alternative ant versions and JVMs can also
   be used.
   </p>

   <p>
   Most attributes and elements supported by the <tt>&lt;java&gt;</tt> and
   <tt>&lt;ant&gt;</tt> tasks are supported here.
   </p>

   @author Steven Morgan
   @version 1.0
 */
public class AntForkTask extends Task
{
    /**
       Properties that should not be passed along!
     */
    private static final Set FILTER_PROPERTIES = new HashSet();
    static
    {
        FILTER_PROPERTIES.add("line.separator");
        FILTER_PROPERTIES.add("file.separator");
    }

    /**
       The Project for the forked ant task.  A separate Project is required.
       If the task were forked from the current Project, then the Process
       would not be garbage collected after completion due to hanging references.
     */
    private Project javaProject;

    /**
       Whether or not environment variables will be inherited.
       The default value is <tt>true</tt>
     */
    private boolean inheritAll = true;

    /**
       The name of the ant build file to be processed.
       There is no default value; this must be specified.
     */
    private String buildFile;

    /**
       The name of the target in the ant build file to be processed.
       There is no default value; the ant build file's default will
       be processed if no target is specified.
     */
    private String target;

    /**
       The location of the ant installation to be used for the forked task.
       The default is the currently executing installation.
     */
    private File antHome = new File(System.getProperty("ant.home"));

    /**
       The Java installation to be used for the forked task.  The default
       is the value in the platform's environment variable JAVA_HOME.
     */
    private File javaHome;

    /**
       Should the emacs flag be indicated for the forked ant task.
       The default value is <tt>false</tt>.
     */
    private boolean emacs = false;

    /**
       The classname of the logger to be used by the forked ant task.
       There is no default value.  If not set, no logger is specified
       to the forked ant task.
     */
    private String logger;

    /**
       The classname of the event listener to be used by the forked ant task.
       There is no default value.  If not set, no event listener is specified
       to the forked ant task.
     */
    private String listener;

    /**
       The filename of the log file to be used by the forked ant task.
       There is no default value.  If not set, no log file is specified
       to the forked ant task.
     */
    private File logfile;

    /**
       The classname of the input handler to be used by the forked ant task.
       There is no default value.  If not set, no input handler is specified
       to the forked ant task.
     */
    private String inputHandler;

    /**
       The properties specified in this task's definition for this specific
       execution.
     */
    private Vector propertyList = new Vector();

    /**
       The properties inherited from the current environment by the forked
       ant task.
     */
    private Properties properties = new Properties();

    /**
       Should the quiet flag be indicated for the forked ant task.
       The default value is <tt>false</tt>.
     */
    private boolean quiet = false;

    /**
       Should the verbose flag be indicated for the forked ant task.
       The default value is <tt>false</tt>.
     */
    private boolean verbose = false;

    /**
       Should the debug flag be indicated for the forked ant task.
       The default value is <tt>false</tt>.
     */
    private boolean debug = false;

    /**
       The Java task generated from the new Project that will be executing
       ant in a separate process.
       @see #javaProject
     */
    private Java java;

    /**
       Create a new AntForkTask.
     */
    public AntForkTask () {}

    /**
       Initialize this AntForkTask.  The separate Project and the Java task
       are created and fixed values are set here.

       @throws BuildException if thrown during the creation or initialization
       of the new Project or the Java task
     */
    public void init () throws BuildException
    {
        javaProject = new Project();
        javaProject.setJavaVersionProperty();

        javaProject.addTaskDefinition("java",(Class) getProject().getTaskDefinitions().get("java"));
        java = (Java)javaProject.createTask("java");
        java.setOwningTarget(getOwningTarget());
        java.setTaskName("antfork");
        java.setFork(true);
        java.setClassname("org.apache.tools.ant.Main");
        java.setDir(getProject().getBaseDir());
    }

    private void addProperties (Map propertyMap)
    {
        for (Iterator propIter = propertyMap.entrySet().iterator();
             propIter.hasNext();
             )
        {
            Map.Entry nextProp = (Map.Entry)propIter.next();
            java.createArg().setValue("-D" + nextProp.getKey() + "=" + nextProp.getValue());
            log("Added property " + nextProp.getKey() + " -> " + nextProp.getValue(), Project.MSG_VERBOSE);
        }
    }

    /**
       Execute ant in a separate process according to the current configuration.

       @throws BuildException if the forked ant process could not be executed
                              or if thrown by the Java task
     */
    public void execute() throws BuildException
    {
        javaProject.setInputHandler(getProject().getInputHandler());

        for (Iterator listeners = getProject().getBuildListeners().iterator();
             listeners.hasNext();
             )
        {
            javaProject.addBuildListener((BuildListener)listeners.next());
        }

        if (antHome == null)
        {
            throw new BuildException("antHome must be set");
        }

        if (!antHome.exists())
        {
            throw new BuildException("antHome does not exist: " + antHome);
        }

        Environment.Variable antHomeSysVar = new Environment.Variable();
        antHomeSysVar.setKey("ant.home");
        antHomeSysVar.setFile(antHome);
        java.addSysproperty(antHomeSysVar);

        FileSet antLibs = new FileSet();
        antLibs.setDir(new File(antHome, "lib"));
        antLibs.createInclude().setName("**/*.jar");
        Path classpath = createClasspath();
        classpath.addFileset(antLibs);

        for (Enumeration propEnum = propertyList.elements();
             propEnum.hasMoreElements();
             )
        {
            ((Property)propEnum.nextElement()).execute();
        }

        // Put all properties in one Properties object
        // Inherited properties first
        Properties definedProperties = new Properties();
        if (inheritAll)
        {
            definedProperties.putAll(getProject().getProperties());
            log("Inheriting all: " + definedProperties, Project.MSG_DEBUG);
        }
        else
        {
            definedProperties.putAll(getProject().getUserProperties());
            log("Inheriting user: " + definedProperties, Project.MSG_DEBUG);
        }

        // Remove predefined properties, as best as possible, so as not to confuse ant
        for (Iterator propertyIter = definedProperties.keySet().iterator();
             propertyIter.hasNext();
             )
        {
            String name = (String)propertyIter.next();
            if (name.startsWith("ant.") || name.startsWith("java.") ||
                name.startsWith("os.") || name.startsWith("user.") ||
                name.startsWith("sun.") ||
                FILTER_PROPERTIES.contains(name))
            {
                propertyIter.remove();
            }
        }

        // Specified properties second, thus overriding any
        // inherited properties
        definedProperties.putAll(properties);
        log("Explicitly defined: " + properties, Project.MSG_DEBUG);

        // Remove basedir or ant will be unable to set it
        definedProperties.remove("basedir");

        log("All defined properties for antfork: " + definedProperties, Project.MSG_DEBUG);
        addProperties(definedProperties);

        if (javaHome == null)
        {
            // Messy to get JAVA_HOME, but this is how other ant tasks do it
            Project tempProject = new Project();
            Property envProperties = new Property();
            envProperties.setProject(tempProject);
            envProperties.setEnvironment("env");
            envProperties.execute();
            javaHome = new File((String)tempProject.getProperty("env.JAVA_HOME"));
            tempProject = null;
            envProperties = null;
        }

        if (!javaHome.exists())
        {
            throw new BuildException("javaHome does not exist: " + javaHome);
        }

        // Reproducing classpath logic from the ant scripts.
        // Add tools.jar and classes.zip as needed.
        File javaLibs = new File(javaHome, "lib");
        if (javaLibs.exists())
        {
            File toolsJar = new File(javaLibs, "tools.jar");
            if (toolsJar.exists())
            {
                classpath.createPathElement().setLocation(toolsJar);
            }
            toolsJar = null;

            File classesZip = new File(javaLibs, "classes.zip");
            if (classesZip.exists())
            {
                classpath.createPathElement().setLocation(classesZip);
            }
            classesZip = null;
        }
        javaLibs = null;

        if (emacs)
        {
            java.createArg().setValue("-emacs");
        }

        if (quiet)
        {
            java.createArg().setValue("-quiet");
        }

        if (verbose)
        {
            java.createArg().setValue("-verbose");
        }

        if (debug)
        {
            java.createArg().setValue("-debug");
        }

        if (logfile != null)
        {
            java.createArg().setValue("-logfile");
            try
            {
                java.createArg().setValue(logfile.getCanonicalPath());
            }
            catch (IOException ioe)
            {
                throw new BuildException("Could not determine canonical path for " + logfile, ioe);
            }
        }

        if (logger != null)
        {
            java.createArg().setValue("-logger");
            java.createArg().setValue(logger);
        }

        if (listener != null)
        {
            java.createArg().setValue("-listener");
            java.createArg().setValue(listener);
        }

        if (inputHandler != null)
        {
            java.createArg().setValue("-inputHandler");
            java.createArg().setValue(inputHandler);
        }

        if (buildFile != null)
        {
            java.createArg().setValue("-buildfile");
            java.createArg().setValue(buildFile);
        }

        if (target != null)
        {
            java.createArg().setValue(target);
        }

        // The REAL work
        java.execute();

        // Null everything to allow garbage collection
        javaProject = null;
        java = null;
        antHome = null;
        propertyList = null;
        properties = null;
        javaHome = null;
        logfile = null;

        // Request garbage collection so that the underlying Procses
        // object doesn't hang around longer than necessary.
        System.gc();
    }

    /**
       If true, pass all properties to the new Ant project.
       Defaults to <tt>true</tt>.

       @param value <tt>true</tt> to inherit the current execution's
                    properties, <tt>false</tt> to not inherit
     */
    public void setInheritAll(boolean value)

    {
        inheritAll = value;
    }

    /**
       The build file to use.

       @param f the ant build file to be processed by the forked ant task
     */
    public void setBuildfile(String f)

    {
        this.buildFile = f;
    }

    /**
       The target of the new Ant project to execute.
       Defaults to the new project's default target.

       @param s the target in the ant build file to be processed by the
                forked ant task
     */
    public void setTarget(String s) 

    {
        this.target = s;
    }

    /**
       Set the emacs flag for the forked ant task.

       @param b <tt>true</tt> if task labels should be dropped,
                <tt>false</tt> to display them
     */
    public void setEmacs (boolean b)
    {
        this.emacs = b;
    }

    /**
       Set the quiet flag for the forked ant task.

       @param b <tt>true</tt> if the forked ant task should be quiet,
                <tt>false</tt> if it should report messages normally
    */
    public void setQuiet (boolean b)
    {
        this.quiet = b;
    }

    /**
       Set the verbose flag for the forked ant task.

       @param b <tt>true</tt> if the forked ant task should be verbose,
                <tt>false</tt> if it should report messages normally
    */
    public void setVerbose (boolean b)
    {
        this.verbose = b;
    }

    /**
       Set the debug flag for the forked ant task.

       @param b <tt>true</tt> if the forked ant task should be debugged,
                <tt>false</tt> if it should report messages normally
    */
    public void setDebug (boolean b)
    {
        this.debug = b;
    }

    /**
       Property to pass to the new project.
       The property is passed as a 'user property'

       @return a new AntForkProperty
       @see AntForkProperty
     */
    public Property createProperty()
    {
        Property property = new AntForkProperty();
        property.setTaskName("property");
        property.setProject(javaProject);
        propertyList.add(property);
        return property;
    }

    /**
       Set the ant installation to be run in the new process.

       @param f the desired ant installation's location
     */
    public void setAntHome (File f)
    {
        this.antHome = f;
    }

    /**
       Set the Java installation to be run in the new process.

       @param f the desired Java installation's location
     */
    public void setJavaHome (File f)
    {
        this.javaHome = f;
    }

    /**
       Set the log file to be used by the forked ant process.

       @param f where ant should log its results
     */
    public void setLogfile (File f)
    {
        this.logfile = f;
    }

    /**
       Set the logger to be used by the forked ant process.

       @param s the name of the desired logger class
     */
    public void setLogger (String s)
    {
        this.logger = s;
    }

    /**
       Set the event listener to be used by the forked ant process.

       @param s the name of the desired event listener class
     */
    public void setListener (String s)
    {
        this.listener = s;
    }

    /**
       Set the input handler to be used by the forked ant process.

       @param s the name of the desired input handler class
     */
    public void setInputHandler (String s)
    {
        this.inputHandler = s;
    }

    /**
       Set the classpath to be used for the forked Java execution.  This
       is the classpath under which ant will be executed.

       @param s the Path to be used for the JVM executing ant in the
                new process
     */
    public void setClasspath(Path s)
    {
        java.createClasspath().append(s);
    }
   
    /**
       Creates a nested classpath element to be added to the classpath
       for the new process.

       @return a new Path that is part of the classpath the Java process
               will use for running ant
     */
    public Path createClasspath()
    {
        return java.createClasspath();
    }

    /**
       Adds a reference to a CLASSPATH defined elsewhere.

       @param r the classpath to be used by the Java process when running ant
     */
    public void setClasspathRef(Reference r)
    {
        javaProject.addReference(r.getRefId(), r.getReferencedObject(getProject()));
        java.createClasspath().setRefid(r);
    }

    /**
       Set the command line arguments for the JVM that will be running ant.

       @param s the arguments to be used, delimited by spaces
     */
    public void setJvmargs(String s)
    {
        java.setJvmargs(s);
    }
       
    /**
       Creates a nested jvmarg element describing a single command line
       argument for the JVM that will be running ant.

       @return a new command line argument for the JVM
     */
    public Commandline.Argument createJvmarg()
    {
        return java.createJvmarg();
    }

    /**
       Set the command used to start the JVM.

       @param s the command to start the JVM
     */
    public void setJvm(String s)
    {
        java.setJvm(s);
    }
       
    /**
       Add a nested sysproperty element.  This value will be visible to ant
       in the forked process.

       @param sysp the desired system property for the JVM
     */
    public void addSysproperty(Environment.Variable sysp)
    {
        java.addSysproperty(sysp);
    }

    /**
       Throw a BuildException if process returns non 0.

       @param fail <tt>true</tt> if an error reported by the forked ant process
                   should abort the current ant process, <tt>false</tt> if the
                   current process should continue on
     */
    public void setFailonerror(boolean fail)
    {
        java.setFailonerror(fail);
    }

    /**
       The working directory of the forked ant process

       @param d the directory in which the JVM process should be executed
     */
    public void setDir(File d)
    {
        java.setDir(d);
    }

    /**
       File the output of the process is redirected to.

       @param out the file where the JVM's output will be captured
     */
    public void setOutput(File out)
    {
        java.setOutput(out);
    }

    /**
       Set the maximum memory that the JVM may use.

       @param max the maximum memory to formatted as expected by the JVM
     */
    public void setMaxmemory(String max)
    {
        java.setMaxmemory(max);
    }

    /**
       Specify the Java version to which the JVM complies.  This is
       currently used by the Java command line support in ant to determine
       the maximum memory paramter (-mx for 1.1, -Xmx otherwise).

       @param value the Java version supported by the JVM (i.e., "1.3.1")
     */
    public void setJVMVersion(String value)
    {
        java.setJVMVersion(value);
    }

    /**
       Use a completely new environment.

       @param newenv <tt>true</tt> if the execution environment should be
                     duplicated for the forked process, <tt>false</tt> if not
     */
    public void setNewenvironment(boolean newenv)
    {
        java.setNewenvironment(newenv);
    }

    /**
       Add a nested env element - an environment variable.

       @param var the environment variable to be set for the forked process
     */
    public void addEnv(Environment.Variable var)
    {
        java.addEnv(var);
    }

    /**
       This subclass of Property is used internally by AntForkTask to capture
       all the properties being set for the forked ant task.
     */
    private class AntForkProperty extends Property
    {
        /**
           By overriding addProperty, all property name/value pairs are
           captured so that they can be saved to a separate file.

           @param name the property name
           @param value the property value
         */
        protected void addProperty (String name, String value)
        {
            properties.put(name, value);
        }
    }
}

No comments:

Post a Comment