## Wednesday, April 3, 2013

Apache Ant is a Java-based build tool, and because it's based on Java, it is entirely cross-platform. It comes with a huge list of available tasks that you can run simply by including them in a build.xml file, but if there's something you want it to do and no existing task can do it, it's easy to extend Ant just by writing a new Java class. Here's how to write your very own Ant tasks.
Before you get started, it's always best to check the list of existing Ant tasks, and the list of external (not included in default Ant) tasks submitted by developers outside the Ant project and not directly supported by the Ant developers. While writing a new task can be fun, there's no need to reinvent the wheel if someone has already done the work for you. If they haven't, because you've found something new it would be handy to do or have unusual specifics you need to manage, you can extend Ant for yourself.

### A very basic new task

To write a new task, you need to extend the Ant Task class. (You can theoretically manage without doing so, but extending Task gives you access to a bunch of useful methods and setup, so it's sensible to use it.) Let's see how with a simple HelloWorld example. Create a new directory hellotask, and a file src/HelloTask.java that contains:


public void execute() {
String message = getProject().getProperty("ant.project.name");

log("Project: " + message);
log("Hello from " + getLocation());
}
}

The only method you absolutely must have to extend Task is execute(). Here, we're calling the logger twice to output two lines. getProject(), getProperty(), and getLocation() are all inherited from the Task class, and do what you'd expect.
To run and compile this file, obviously we want to use Ant! Here's the build.xml file:



Most of this should be familiar if you read my tutorial on Ant buildfiles; it cleans things up, compiles, and creates a JAR from the source code. Run ant jar to build HelloTask into a JAR.
The final section is the bit that actually runs the task. The taskdef (task definition) line tells Ant what to do when it sees later on. (With predefined tasks, you don't need this line because Ant already knows how to handle them.) Note that in this setup, where we might be deleting and recompiling the JAR, the taskdef line must be inside the target block. If you put the definition earlier in the file, and HelloTask.jar has been deleted (by running ant clean), Ant won't find the JAR and you'll get an error. Later, if you were to import the JAR elsewhere and just run the task, you could put this line at the top of the file.
Finally, we set up the hello target to run . Type ant hello and you should see output a bit like this:
Buildfile: /home/juliet/coding/ant/mytask/build.xml

compile:

jar:

hello:

BUILD SUCCESSFUL Total time: 0 seconds


### Handling properties in a task

Currently, this task doesn't take parameters. Let's rewrite it so we can pass something in – the name of the task's owner, for example:
public class HelloTask extends Task {
private String owner;

public void execute() {
String projectName = getProject().getProperty("ant.project.name");

log("Project: " + projectName);
log("Hello to " + owner + " from " + getLocation());
}

public void setOwner(String owner) {
this.owner = owner;
}
}

Note that the setter method (setOwner()) must match the name of the variable you wish to set. Ant then generates a matching getter method at compile time, and uses this when the task is run. (You could of course also write your own getter method if you prefer.) Edit build.xml to specify the parameter:



Run ant hello and your output should look like this:


compile:
[javac] Compiling 1 source file to /home/juliet/coding/ant/mytask/classes

jar:

hello:

BUILD SUCCESSFUL Total time: 0 seconds


Finally, let's take a look at a more complicated real-world example – an Ant task I wrote to deal with LaTeX files. This is the first, ultra-basic version, which just generates DVI output from a given LaTeX input file. Save this as src/MakeLatex.java:
public class MakeLatex extends Task {
private String input;

public void execute() {
log("Building file: " + input);
String runLatex = "latex " + input;
try {
String line;
Process p = Runtime.getRuntime().exec(runLatex);
while ((line = input.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}

public void setInput(String input) {
this.input = input;
}
}

The setter method is familiar. In execute(), we send a log message, then create the command to run from the input provided. This command is then run externally with Runtime.getRuntime().exec(), and the output collected and printed to screen with a BufferedReader and the while loop.
To run this, edit the build.xml file to include this new target:



The existing jar target builds the new class into the HelloTask.jar file. Obviously if you were using this for real you'd want to build it into its own jarfile and save it somewhere else so you could use it in future.
You also need a test .tex file. (Note: The test file is saved as test.tex, but you don't need the .tex extension when setting input in build.xml; LaTeX will assume it.):
\documentclass[a4paper]{article}
\begin{document} Test!
\end{document}

Run ant latex to build your file, then look at test.dvi in your preferred DVI viewer to see the output.
While this approach generates a test.dvi file, it doesn't open it for the user to look at, and it generates a lot of output noise. Also, these days most people prefer PDF output to DVI output – I certainly do! So here's an improved version, which takes an output and a verbosity value:

public class MakeLatex extends Task {
private String input;
private String output = "pdf";
private Boolean verbose = false;
private String latexCommand;
private String showOutputCommand;

public void execute() {
log("Building file: " + input);
if (output.equals("pdf")) {
latexCommand = "pdflatex";
showOutputCommand = "xpdf " + input + ".pdf";
} else if (output.equals("pdf")) {
latexCommand = "latex";
showOutputCommand = "xdvi " + input + ".dvi";
} else {
log("Don't know how to handle output " + output + "!");
log("Generating and showing you the dvi instead");
latexCommand = "latex";
showOutputCommand = "xdvi " + input + ".dvi";
}
String runLatex = latexCommand + " " + input;
try {
String line;
Process p = Runtime.getRuntime().exec(runLatex);
if (verbose) {
while ((line = input.readLine()) != null) {
System.out.println(line);
}
}
p = Runtime.getRuntime().exec(showOutputCommand);
} catch (Exception e) {
e.printStackTrace();
}
}

public void setInput(String input) {
this.input = input;
}
public void setOutput(String output) {
this.output = output;
}
public void setVerbose(Boolean verbose) {
this.verbose = verbose;
}
}

And here's the accompanying buildfile, which specifies the output format and verbosity. If your buildfile doesn't specify output or verbosity values, the task defaults to PDF and to non-verbose:



Ant will now generate the sort of output you prefer, and fire up the specified viewer.
Finally, one last improvement: Let's pass in a filename from the command line. Ant can do this if you add this code to build.xml:


Must specify input file


This sets a condition property, params.set, based on whether the "input" value is set. When we build the latex target, Ant first checks the status of params.set, and fails the build (stopping there) unless it is true. If it is true, the value is passed into the task, and off we go. You call this like so (note the -D argument):
 ant latex -Dinput='test'
As it stands, this task is very basic. You could add more options to it; for example, you could add other output formats, or a bibtex option. You could also do some error-checking on the input filename; currently if you set input as test.tex rather than test, the task would fail. Experiment a little with the code to see what else you can do with it, and to get a better grasp of writing a task to do exactly what you want it to.