What Is Ant, Really?
Learn about the structure of the Ant build system and how to make it work for you. Understand how you can harness it in different environments and what the pitfalls are.
Join the DZone community and get the full member experience.
Join For FreeAnt is a bit of a mystery bag. Its behavior is often obscure until you come to look at its code. Then you find that it consists of a number of fairly simple facilities that are often explained from a bottom-up detailed and technical viewpoint and not from a top-down architectural perspective. This article aims to provide the missing top-down view. It is targeted at an audience of software engineers. Armed with this article, and some solid opinions on when and when not to use this tool, you should be able to find your way in the anthill.
Ant History, Legacy, and Impact
If you have never heard of Ant, don't worry. Ant is yet ANother Tool in the realm of building software. It was the first attempt at a build tool for Java. When it was conceived, XML was all the rage, and C was the way to express software that Java mimicked. Consequently, Ant was influenced by the thinking of C's make tool. Combining these two trends, Ant is a curious hybrid that does not have make's terseness or stringent reproducibility but does have XMLs verbose syntax. Talk about the best of both worlds.
However, Ant's greatest shortcoming is probably its lack of opinion and firm ideas on the requirements of a Java-based, or indeed any, project build and lifecycle. This leaves developers with lots of freedom to create lots of hard-to-find insidious build bugs. In some ways, Ant is the C++ of build tools: A small core with low pay overhead, substantial possible power, and an explosive potential for man-hour-consuming disaster.
With the soap-box out of the way, let's delve into Ant's concepts and architecture.
Top-level Architecture: A Forest of Targets
At its conceptual top is a dependency mechanism that produces a forest (formally, a Multi-tree) of possible objectives that you as a developer want to reach, known as Targets.
Each Ant project is broken down into a sequence of elements called 'targets' which have identifiers as names. These are usually conventional activities like 'compile,' 'test,' 'archive,' etc. They internally consist of tasks, the actual units of execution.
Each target can declare dependencies on other targets. As a result, each project contains a number of execution trees. Either the user can select one for execution by name, or a special 'default' marker is run.
This also means that at the level of targets, Ant is not a programming language. No matter what, for a single Ant run, it can only ever traverse forward in the target dependency tree.
Below is a class diagram of these core concepts. There are numerous tools to make the dependency tree of targets visible:
- Ant Task Dependency Graphs using XSLT transforms.
- Vizant — Ant task to visualize buildfile.
Parsing
Note that the previous section talked about projects, not build files. While Ant projects are almost always written in XML and parsed by an XML parser, this does not necessarily have to be the case. The parser is replaceable by replacing the Ant frontend known as the ProjectHelper. Indeed the tree of actual project dependencies can be constructed in-memory as a result of program execution.
Gradle is Ant on Steroids
This idea is reflected in parts of the Gradle build system. In Gradle, the equivalent of a specific ProjectHelper is generated as a compiled class with defined inputs. As a result this compiled program very quickly builds a minimal execution tree that is subsequently executed in a well-defined way. Of course, Gradle went its own infrastructure-way a long time ago, but the origins are still visible in its two-phase execution mechanism, its ease of reuse of Ant Task classes, and its support of Ant's ivy repository manager.
Ant Parsing Trivia
There are two interesting asides about the parsing process:
- Ant has a concept of 'prelude' or default: As stated above, its operations are usually scoped into 'targets.' However, there is a top-level of operations that are not scoped. As the manual states, since Ant 1.6.0, every project includes an implicit target that contains any and all top-level tasks [...]. This target will always be executed as part of the project's initialization [...].
- Ant is theoretically 'polyglot': It can 'assemble' a project from a source with different syntactical representations. It can 'import' other sources from the Prelude and the corresponding ProjectHelper will parse them.
Side-effects via Tasks
Ant targets execute by calling sequences of Java-based tools packaged via an interface called Task. There are quite a lot of them packed in with the distribution, some more contributed and even more floating around on the internet. It is not hard to hack up your own, but it can be challenging to do it properly. Shameless plug here: Part 2 of this series shows how to do it.
The parser instantiates each Task of the target, in turn, feeds it parameters, usually in the form of strings, and then fires the tasks' execute method.
Objects in Ant: Custom Components
The previous section skirted a solid definition of the types of parameters. It said that parameters are usually strings. This means that occasionally they are not.
Let us take an example: How would you coordinate a video file processing toolchain? You could write the parameters for the files multiple times, every time you were feeding them to a tool. And then you could add the parameters for each tool execution. Mixed up with the syntax of XML, this would be so ugly and lengthy, you would want to run back to a bash script or Powershell.
To avoid this, Ant allows us to define Java objects as parameters. To clarify their possible properties, these have to have associated types. These additional types are declared using a typedef instruction. In Ant, these special types are called 'Custom components,' but the manual clarifies that in fact, they are simple Java Beans:
A custom component is a normal Java class that implements a particular interface or extends a particular class or has been adapted to the interface or class. [...] One defines attributes and nested elements by writing setter methods and add methods.
So, assuming we create the ImageFile type, now you can create:
<imageFile type=”mkv” path=”~/downloads/murderMystery.mkv”/>
But how does that help, unless you can pass this object by reference in some way?
Object References and the Memory Model
Indeed, the Ant project has a form of main object memory or heap. It is a global add-only Map<String,Object>. This means that references can be defined once with an identifier that is unique across the project. The description of References in the manual shows this in detail. XML syntax enforces this naturally using the 'id
' attribute.
Now any place that uses these elements can reference them. Pointers can come from multiple places, using a 'refid
' notation. So, assuming we had defined:
xxxxxxxxxx
<imageFile id=”cbFun” type=”mkv” path=”~/downloads/murderMystery.mkv”/>
We could now extract audio somewhere else by reference:
xxxxxxxxxx
<extractAudio refid=”cbFun” targetFormat=”mp3” path=”timmy.mp3”/>
Reproducibility and Background State
Of course, there is no requirement that these objects have to be immutable. They can function as live inputs, communication pipes, storage buffers, or anything else you might want, with the corresponding state-based consequences.
The behavior of your build is only reproducible if all types used in the build are based on immutable classes and your environment to the tasks used is identical. If you are experiencing unusual behavior, this is the formal aspect you have to control.
Polymorphic Custom Components
Of course, you have already spotted another aspect of this approach: If these custom components are indeed just Java classes, they can also be based on interfaces and abstract classes. Related 'Function Objects' can be defined based on a common API and accepted by a set of tasks.
For our media example, you could think of something like 'codec settings' that combine a codec with its configuration and can be referenced by a number of processing tools across the build.
Stereotypical Custom Components: Built-ins
There are a large number of custom types available and the description in the manual does not really reveal the structure or intent. I only really understood what was going on when I listed all derived classes of DataType in my Eclipse IDE. Below is a screenshot that shows the class hierarchy of the FileSet, a utility object to describe and access a group of files that is used as input by several file-processing tasks.
And just in case you thought my example of video-processing was contrived, here is the built-in ImageOperation that allows you to draw colored ellipses and squares with Ant and then rotate and scale them. Because why should you use SVG when you can have Ant?
I am not going to delve further into the semantics of the various types and associated tasks here, as this is more a question of design patterns than of the architecture of the tool itself.
Attributes, Properties, and Interpolation
While tasks can take all types of objects as parameters, either via nesting construction or via reference to existing objects, the most common type, and the one that suggests itself in the XML syntax is a String type object that appears in Ant's XML syntax as an Attribute of an Element.
This type of parameter is extremely common and is also easily defined. It also seems to suggest that these parameters are usually text when they are passed to tasks, which is generally true, and we will come to the differences later.
Ant's properties look simple but are easily the most complex part of the framework when a lens is applied to the details. They can also be utterly confounding. To keep the story cohesive, we will describe the original historical design below, then look at the forces that prompted the change, and finally present today's highly flexible system.
Traditional Design
In the original Ant design, text parameters received a special treatment to avoid repetition with a memory model called Properties. Properties are an add-only store of type Map<String, String>. Underlying is simply a write-once set of Java Properties.
The Big Property Bash
The original design of Ant used string interpolation to replace references to keys with values based on the simple replacement notation of the Unix bash shell. Here is an example:
xxxxxxxxxx
<elem param=”${key}”/>
In this example, a task or custom component 'elem
' is constructed using a parameter 'param' that is drawn from what Ant has stored in its map under the key 'key
.'
Auto-Boxing Properties
Not all tasks take input from string parameters. In theory, there is no reason why a parameter should be different from a nested element. To address this, early versions of Ant already provided implicit parsing to a fixed set of types, largely parses of primitive types, path notations, and enums.
Bootstrap and Defaults
The set of properties that was present in a project on start-up, the 'property boot-strap,' was composed of two sources:
- Properties of the JVM System, including any System properties.
- Properties of the Ant Runtime at first startup.
After this point, any Task can make a call to its project's setUserProperty()
method and define a new property. The most common task to use for this purpose is the Property Task.
'Most Definitely Not Variables.'
The documentation of the Property Task makes it clear that Ant user properties and Java properties are not the same:
"Properties are immutable: whoever sets a property first freezes it for the rest of the build; they are most definitely not variables."
If you ever wonder what properties you can see from where you are in the build, the Echoproperties Task will show you what is available. Please note that this can vary quite considerably by how you got there, as the definition of properties can be conditional and will vary by the targets traversed to the current point of execution.
The Anything-Goes Redesign
The traditional design had some shortcomings that caused increasing rumblings in the undergrowth of the user community. Below are a few:
- The parser for the replacements was dumb and would not allow double-pointer resolutions like
${${follow-me}}
, or compositions like${${follow}${me}}
. - You could not use regexes and other Unix pocket-knifes like sed to cut and join directly in the expression.
- You could not 'get' anything from 'object-space.' Items defined as references based on custom components could not be used as attributes in the build.
- There are no local variables.
- There are no external or scoped property stores.
- The syntax for properties was fixed to the bash syntax.
Help! It's the mighty Property-Helper
Faced with these issues, Ant was redesigned in version 1.6 and later 1.8 to eventually replace the core property resolution system with a framework. All of this complexity hides behind the innocently-named PropertyHelper.
Each Ant project has one PropertyHelper
. It fires every time an attribute value is retrieved and delegates responsibilities to pluggable extensions of four different types, connected to layers of the resolution process.
Finding Keys, Composing Value
The top layer resolves any properties found in the attribute string to arrive at a resulting value. This usually implies that shorthand is expanded to a longer expression. Consequently, the interface responsible for this is called the PropertyExpander.
When it expands a string, the PropertyHelper
delegates the actual parsing to parseProperties inside the ParseProperties
class which in turn uses the PropertyExpander delegates to find properties inside the string. The PropertyExpanders
then call back to the PropertyHelper
to resolve the property names into values. This game repeats until a value is found.
Obtaining Values from Names
When the PropertyHelper is asked to resolve a property name into a property value, PropertyEvaluators are responsible for this functionality. There are two major use-cases for this facility:
- Special protocols: for example, a notation like
${toString:refid}
can be designed to render object references as text. In fact, this protocol is now a built-in feature of Ant. Or a whole lot of UNIX string operations can be implemented. - Special storage: the Local Task allows to open up a namespace for properties that are available in the scope, but discarded once the defined scope, usually the target, ends. This means that the evaluator has to identify it is called within the scope and then redirect the access first to the property store local to the scope, and only after that to the outer property stores.
Alternative Property Storage and Enumeration
The entrypoint for creating Properties is in the PropertyHelper
itself. It must be identical for all properties, irrespective of who stores the property in the end, because all tasks must use it transparently. As a result, when a property is defined, it needs to be presented to all potential 'storage providers.' Only after none of the delegates wanted to take ownership of this property, the PropertyHelper
will use the Projects default property store.
The interface that provides this facility is the PropertySetter.
We had said previously that the Echoproperties Task gives information on what properties are visible at a certain location in the build. In the classic design, this was simply an iteration over the current table of properties. After the redesign, however, there are now multiple stores where the information could be. Again, in order not to write a special code Project.getPropertyNames() should work as it did before. This means that every alternative property storage needs to provide a PropertyEnumerator that can be called to obtain the list.
The Truth in Pictures
Using Your Own
All of the above is worth little if you cannot change the build to perform property resolution the way you want it to. This way we are providing a template of the use of the PropertyHelper Task here.
<typedef classname="org.example.MyPropertyEvaluator" name="mypropertyevaluator"></div>
<mypropertyevaluator id="evaluator"></mypropertyevaluator>
<propertyhelper>
<delegate refid="evaluator"></delegate>
</propertyhelper>
The first line declares the type of the custom component, as explained earlier. There are several ways to do that. Take it as is, for now, details are below in the section on Adding to Ant. The second line creates an object or instance of the type. Lines three and five declare the change to the PropertyHelper
and line four adds the delegate by reference.
Note that PropertyHelper
uses the configured delegates in LIFO order. For example, the delegate added by this task will be consulted before any previously defined delegate and in particular before the built-in ones.
Ant Object Syntax Trivia: ComponentDef ***
Occasionally you will find the following form instead of the one above:
<componentdef classname="org.example.MyPropertyEvaluator" name="mypropertyevaluator"></div>
<propertyhelper>
<mypropertyevaluator></mypropertyevaluator>
</propertyhelper>
The Componentdef Task is a shorthand notation that combines creation class-loading and object creation. It is intended for use in tasks that contain sub-elements to give the object reference by name (<mypropertyevaluator/>
) instead of by explicit reference (<delegate refid="evaluator"/>
). Looks slicker, but it is the same thing.
Adding to Ant Behaviour
Ant was created in the days when Java applications were so small that a library version collision and class-loader hell were far, far away. Adding something to Ant just meant dropping your tool in and writing a task definition. Both would be loaded on Ant's classpath.
Et voila. Extension done. The next section is intended to give you a foundation for understanding some difficulties in extending Ant. You can skip it if you just want the 'how' and not the 'why.'
Class Excursion to Hell
For those who have not had exposure to this impressive art let me walk you to the Java history museum and introduce you to the Hieronymus Bosch picture 'the pleasures of classpath hell.' For those of you who have had the experience already, you may take a moment to study the condition of today's software quality by looking at The Scream while I explain or you can look over my shoulder and leave comments under the article to point out where my description of the matter was lacking or imprecise.
ClassLoaders
ClassLoaders are classes that, as the name states, load classes. Usually, there is one per application for the whole virtual machine. Whenever an object is created, its class name is turned into a key. The key is passed to the classloader of the object called the 'new
' method. The classloader
takes the class name, goes away, and creates the data structure for the new class. That data structure then has code to create the new instance. And that code is called in a second phase to make the instance (instantiation).
Different sources
Usually, the classloader replaces dots '.
' with slashes '/
,' goes to the directory that holds the classes, loads a file, and then fills the classes' data. This works fine until you want to load your classes from other sources: By downloading from the web, generated at runtime (yes, that is common!), etc. To ensure that this is possible, you can designate a different classloader for the process. It can also go away and load a class from an alternative source.
Collision: Different Source, Same Name
Now let us explore a typical problem: Assume an existing object already has a pointer to a com.myorg.Foo
object. Foo's class was loaded from a disk location. It has a memory structure com.myorg.Foo@C:/my/classpath
. Now another class uses a classloader
to create a Foo
object from a class file that is on a server far away. That file has the structure of com.myorg.Foo@http://my.server.com/classes
. Unless we are very lucky, the different structures of these two files will create different objects. But from inside the JVM, there is no way of telling that this difference exists, because the language of Java classes does not describe class loaders at all. Given how quick we would like instantiation to be, no classloader would have time to check the compatibility of the class files anyway. Determining compatibility, in general, is also an impossible mission. For these reasons, when an object reference is assigned in Java, the JVM checks the class name and class loader. If they match up, the cast succeeds. If not, you put a puzzled post on stack overflow.
The General Contract: Upward Delegation
To avoid this situation from the start, classloaders normally try to follow a best practice or general contract in the implementation. The new classloader always asks the current classloader if it already knows of a class by that name. It delegates the search. If this pattern is observed, clashes like the one described above can never happen. However, if you understand the mechanism, you can implement all sorts of smart contraptions using it. Major examples for class-loader control as architecture are the Spring Framework, Jakarta Enterprise Beans, and the OSGi architecture.
Dynamic Tasks and Types
To load new data types, a.k.a., custom components and new tasks, Ant has added two tasks that perform class loading from within the script at runtime. These are the Typedef Task and the TaskDef Task, respectively. The Typedef task documentation has the following warning for the traveler:
If you are defining tasks or types that share the same classpath with multiple taskdef
or typedef
tasks, the corresponding classes will be loaded by different Java ClassLoaders. Two classes with the same name loaded via different ClassLoaders are not the same class from the point of view of JVM, they don't share static variables and instances of these classes can't access private methods or attributes of instances defined by 'the other class' of the same name. They don't even belong to the same Java package and can't access package-private code, either.
We will discuss in the following sections how Ant has historically handled these issues and the solution it eventually arrived at.
Classloader-Control by Hand
When dynamic type-loading was introduced, new classloaders were created for every definition. To support shared classloaders, the classloaders were given a name. To share among different components that name could be used as a key. The manual recommended to...
[...] use the
loaderref
attribute and specify the same name for each and everytypedef
/taskdef
— this way the classes will share the same ClassLoader.
loaderRef | The name of the loader that is used to load the class, constructed from the specified classpath. Use this to allow multiple tasks/types to be loaded with the same loader, so they can call each other. Since Ant 1.5 |
However, each definition can have a classpath, as shown by the attributes below. If the loader is shared among the components, then the classpath has to be identical or missing. The manual says:
Note that the typedef
/taskdef
tasks must use identical classpath definitions (this includes the order of path components) for the loaderref
attribute to work.
classpath |
The classpath to use when looking up classname. | No |
classpathref |
A reference to a classpath to use when looking up classname. | No |
Component Classloaders with Antlib
The manual classpath management was a brittle affair with a number of shortcomings:
- It required the build author to think about Java infrastructure which was not related to the problem she was trying to solve.
- Hierarchies of tasks and custom components were difficult to develop and use, as the script needed to be specifically designed to accommodate the reuse.
- Builds were not portable, as libraries needed to be shipped with the build setup.
These issues led to the design of a facility for Ant libraries, antlib for short. An antlib removes the ad-hoc definitions of related types from the build-file and packages them together. Consequently, the manual concludes:
The best way to load several tasks/types that are supposed to cooperate with each other via shared Java code is to use the resource attribute and an antlib descriptor.
Antlib Defined
An AntLib is an XML file that groups task definitions and supporting types they may need. For each of these features, there is a corresponding XML element.
Element Defined | Purpose, Defines ... | Also Outside antlib? |
Type | Classes that tasks can use to store and describe complex data as objects, referenced by a unique name. | Yes |
Component | A type intended as a parameter in the parameterization of other types, and whose name can be ambiguous. | Yes |
Task | A task-based on a Java binary implementation. | Yes |
Macro | A task by packing together a sequence of tasks and giving it a name. | Yes |
Preset | A task by taking an existing one and nailing down its parameters. | Yes |
Script | A task by passing a script to an external script interpreter. | Yes |
InputHandler | An input handler to read runtime synchronous build input from a source. https://ant.apache.org/manual/api/org/apache/tools/ant/taskdefs/Input.Handler.html | No |
AntLib Namespace | A task that registers the XML namespace (URI) as a namespace for attributes. |
Yes |
We will first look at a simple example of a descriptor and then at the function and purpose of each of the content elements in turn.
Descriptor
Below is a build file used for testing an asynchronous communication package for our companies' model-based AI system and the descriptor of the Ant library that backs it.
At the root level, the project defines the Ant library. Because this definition happens in the prelude, it is executed before all targets. The library is linked is defined for the URI 'antlib:com.codebots.ant.botlearn
' and the 'antlib:
' protocol triggers the retrieval from the classloader under the conventional path 'com/codebots/ant/botlearn/antlib.xml
.'
The manual says this:
The project element maps that URI to a prefix 'cb
.' This means that the mention <cb:get />
gets expanded to '<antlib:com.codebots.ant.botlearn:get' />
,' which is the task's full name. When Ant arrives at the invocation, it finds the task defined with this key and executes it. This default namespace expansion is described in detail in the manual.
xxxxxxxxxx
<project name="project" default="retrieve" xmlns:cb="antlib:com.codebots.ant.botlearn">
<taskdef uri="antlib:com.codebots.ant.botlearn" />
<target name="retrieve">
<cb:get answer="weatherPreference" as="localWeather" />
<echo file="out.txt" message="I like weather that is '${localWeather}', like at home." />
</target>
</project>
xxxxxxxxxx
<antlib>
<taskdef name="ask"
classname="com.codebots.ant.botlearn.Question" />
<taskdef name="runLater"
classname="com.codebots.ant.botlearn.Dispatch" />
<taskdef name="get"
classname="com.codebots.ant.botlearn.Retrieve" />
</antlib>
Types and Components
As we recall from the section on the Property-Helper and property resolution, the PropertyExpanders and PropertyEvaluators are Ant Types and can be instantiated by reference. This is exactly what the Apache Props Antlib offers. You use it exactly as you would expect:
xxxxxxxxxx
<typedef uri="antlib:org.apache.ant.props"
resource="org/apache/ant/props/antlib.xml"
classpath="YOUR-PATH-TO/ant-props.jar"/>
<project xmlns:props="antlib:org.apache.ant.props">
...
<propertyhelper>
<props:nested />
</propertyhelper>
You can have a look at the Props Ant Library's Descriptor and dependent code if you are interested.
Macros
The MacroDef Task allows the definition of new Tasks by taking in attribute and entity parameters and embedding them into a sequence of tasks. This allows us to reduce typing for repetitive sequence patterns that can be composed without writing new Task code in Java. Essentially, from an object-oriented perspective, a macro is a form of Factory method pattern.
Defaults
The PreSetDef Task allows the definition of a new Task by setting a number of parameters by default and only passing on access to the remaining parameters. This allows us to abbreviate typing for patterns that occur often and to centralize the setting of defaults across a project. Essentially, from a functional perspective, a preset is a type of currying.
Scripts
The Scriptdef Task allows an escape to define a new Task in a scripting language that one of the Java Scripting access standards has access to. This allows us to reuse readily available chunks of scripting and to make immediate changes to the script-based definitions. Obviously, this reduces the portability of build definitions, as the correct installation of the script infrastructure now needs to be guaranteed.
Build Inputs
In the section on Build Inputs, we will look in detail at the ways information can be read in a dialogue form from external sources. Suffice to say here that Ant has a framework to address this issue. At its core is an interface known as the InputHandler, responsible for obtaining, validating, and forwarding the input. InputHandlers are fairly specific, so they receive their own setup for definition in the antlib construct.
Internal Referencing
There are two important aspects to use of elements declared in Ant libraries: Referencing elements within the scope of the library definition and referencing from outside the library definition.
Internal references do not seem to be much of an issue until you realize that the elements are mounted into a namespace during the typedef invocation. This means that the namespace and element that is within the declaration could really be anything. To get around this, elements are temporarily placed in the namespace ant:current, as described in the manual. This allows elements to reference each other mutually during definition.
xxxxxxxxxx
<antlib xmlns:current="ant:current">
<taskdef name="if" classname="org.acme.ant.If"/>
<typedef name="isallowed" classname="org.acme.ant.Isallowed"/>
<macrodef name="ifallowed">
<attribute name="action"/>
<element name="do"/>
<sequential>
<current:if>
<current:isallowed test="@{action}"/>
<current:then>
<do/>
</current:then>
</current:if>
</sequential>
</macrodef>
</antlib>
Cross-Referencing
Tasks in an antlib can reference items from other antlibs. However, antlibs are not a dependency manager. This means that in order to use an antlib
that references certain tasks or custom components, these must have been defined before being called. This can be challenging for complex builds.
xxxxxxxxxx
<antlib xmlns:antcontrib="antlib:net.sf.antcontrib">
<presetdef name="javac">
<javac deprecation="${deprecation}"
debug="${debug}"/>
</presetdef>
<presetdef name="delete">
<delete quiet="yes"/>
</presetdef>
<presetdef name="shellscript">
<antcontrib:shellscript shell="bash"/>
</presetdef>
</antlib>
Antlib Under the Hood
So far we have not described what an Antlib really is under the hood. It turns out that:
Antlib is a task that is used by Ant but cannot occur in a build file. Corresponding to it is a superclass called AntlibDefinition, which has all legal antlib definition tasks as its children. So, under the hood, the antlib execution merely acts like a macro that calls the individual tasks for defining the library in a context and out of sight of the user. The parsing of the definition is left to the ProjectHelper interface. This means that much like builds, antlibs in other syntaxes than XML are theoretically possible.
Structuring Ant Build Files
In the initial section, we had described Ant's build as a forest of targets. This approach is useful for small builds but causes issues in larger builds when the structure has to be broken down. Ant Libraries, as described, help with the definition of reusable tasks, but they do not help with structuring large projects.
There are essentially two dimensions to structuring large projects within Ant:
- Breaking a large project up into several files that define it. This allows dividing responsibilities along the line of whole targets.
- Defining a super-structure of tasks that others can add to.
Again, we will describe this approach from its historical development.
Stone-Age: Including Files using XML Parser Hack
Large Ant-files are difficult to edit, and with multiple authors and the gruesome version control of the day (think RCS and CVS) are not fun to merge. Hence there was interest in breaking Ant files for a project up for distribution of work and reuse.
Since Ant used XML files, it also has an XML parser available, and XML parsers can perform a type of macro expansion known as XML Entity Expansion.
For Ant, this meant using a Document Type Declaration as a means to import things from other files. Not elegant, but obscure to make up for it. Yikes!
State of the Art: Importing and Including Files using a Task
Needless to say, this approach had a whole heap of impediments, apart from its obscure nature. As a result, Ant 1.6 brought a task called Import Task. Import can only be used in the Preamble, outside any other tasks. This is necessary because the project structure needs to be fully loaded before interpretation can begin. Import merges the targets of the imported file with those of the importing file, using some special heuristics to resolve namespace collisions. The logic for this needs to be present in the ProjectHelper that is used by the project.
There is also, not to be confused with the Import Task, an Include Task. Its function is much simpler than the one of the imported Task. It adds all tasks of the included build file, but with a prefix that is determined by the name of the project that the tasks come from. So for a project called 'documentation,' all tasks have the prefix 'documentation
.'
Futuristic: Extension Points
The import goes a fair way to compose a project from various parts, but it does not allow the main file to give an overall recipe in the sense of the Template method pattern.
Often you would like to define something like this at the company level:
- Generate Source
- Build
- Test
- Create Doco
- Package
You want these targets done in this order, but you do not want to wire the specific implementations in detail. Instead, you would like the concrete sub-targets to 'hook' themselves into this target.
We could initially imagine that the 'Generate Source' Target does not need to do anything but later will have Tasks added to it to generate, say an API based on an OpenAPI/Swagger spec.
Let us assume that the build listed above is defined in 'top.xml' and the swagger setup in 'detail.xml.' We would now need to edit 'top.xml' to describe that the target 'Generate Source' depends on 'Produce OpenApi Source Code.' This is not nice, as we wanted to reuse this file across the company. Now we need to make individual copies.
Extension Points overcome this problem. They give other files the ability to 'hook into' a general construct that by itself has no functionality and will stay unaltered. This means that in the abstract we can say that you need to 'Generate Source' before you can 'Build.' From this perspective 'Generate Source' and 'Build' are Extension Points, 'Produce OpenApi Source Code' is an extension of 'Generate Source,' and 'Run JavaC Compiler' is an extension of 'Build.'
The Ant manual provides the technical description of the mechanism. However, extension points are not an Ant-specific invention. Regarding Ant, they are more a retro-fit with a sideways look at the competition. You find the Extension Point mechanism as a general principle of architecture in the original Eclipse framework and applied to build in the Maven build system's concept of build phases. Gradle drives this even further, but there is no good single reference that I can point to.
Cross-cutting Conditions: If and Unless 'Annotations'
In itself, Ant has no means to control flow in a programmatic way. As we had had said previously, Ant is not a programming language. There are some control tasks in the Ant-Contrib Tasks library. They include things like 'if
,' 'for
,' and 'foreach
.' These tasks are mainly intended to 'unroll' repetitive sections of the build to make it more concise to express them. In addition, these tasks only work within a target and not across the whole build.
Since this style of flow-control has been requested for a long time and eventually edits a mechanism, which allows to enable or disable all tasks or targets. This mechanism uses XML attributes and works very much like a programming language annotation. To make sure there are no collisions with existing attributes the special attributes live in reserved namespaces:
xmlns:if="ant:if"
xmlns:unless="ant:unless"
To take effect, the namespace must be combined with a condition attribute as shown in the table below. 'if
' namespace enables a task or target when a condition is met, 'unless
' does the reverse.
attribute | semantic |
true |
value of the attribute evaluates to true |
blank |
value of the attribute is null or empty |
set |
specified property is set |
This results in six combinations. The attribute values are evaluated like all other properties, so an expression like the following is possible, provided there is an evaluator for basic arithmetic in place:
xxxxxxxxxx
<hg:deep-thought if:true=”${42 == 6 * 7}”/>
If such direct evaluation is not available, properties and values can be set using the Condition Task. This task draws from a large set of pre-fabricated custom components called Conditions, that implement logical operators and base predicates.
Running Ant
In this section, we will focus on the actual running of the Ant tool. We will look at how we can start sub-executions of builds, how we can receive information from outside at runtime, how we can track the execution of the Ant build, and how we can start Ant from Java.
Spawning Ant
Ant's task dependency semantic does not allow for loops. So how can you trigger a different dependency 'tree' as part of a build without adverse side-effects like duplicated executions? How do you 'run another build,' while you are already inside one?
There are a number of built-in Ant targets that achieve exactly this goal. The table below lists them in detail. The ones that are included as standard elements by Ant all call a build and give it access to copied properties and references, in as far as the user specifies. These builds can be asynchronous and completely independent. Another version of the commands has been contributed via an Ant library. These versions retain access to the memory system of the calling Ant instance and can add properties and references. As a result, these versions of the command are blocking. Due to their nature, none of these tasks can be used as elements of the preamble.
The calls come in three forms:
- Ant runs another project whose path is specified. This is usually used to call another build as a component that has been contributed by someone else.
- AntCall runs the same project, but a different target. This is usually used to reuse a different master target in the same build.
- Subant runs a single target on multiple other projects. This is usually used as a template method, to achieve a homogenous build over several codebases, or to delegate the execution of a single phase of the build to multiple components.
Call | By value (ant distribution) | By reference (ant-contrib) |
Another project | Ant | AntFetch |
Same project | AntCall | AntCallBack |
Single target on multiple projects | Subant | - |
Questions and Answers: Synchronous Build Inputs
The Input Task allows Ant to obtain information from the user in a synchronous fashion. The build waits for input to come back. This is not done directly using the processes' standard input pipe, but by creating instances of the InputRequest class and handing these to the project which will delegate it to the default InputHandler. Any task that wants to process input can use this facility. Originally, Ant only allowed a single input handler. This causes issues, as these handlers were also used to obtain authentication information and the default handler was echoing the information received, revealing passwords.
As a consequence, instances of the Custom Components that implement the InputHandler can be installed into Ant and references can be passed to the input task or any task that wants to use them. This allows IDEs to install handlers based on native dialogue-boxes, servers to relay build execution remotely and even canned responses from challenge-response pairs loaded from files.
Following the Action: Build Events and Log
Ant has a fairly well-developed logging system, which processes build events, by handing them to Build Listeners. Build Listeners can be installed into a project. This is usually done via the command line option '-listener <classname>
,' or you can add a listener at runtime using the Recorder Task.
The events provided are combinations of the following dimensions:
- Log-Level: ERR, WARN, INFO, VERBOSE, DEBUG.
- Granularity: Build, Target, Task.
- Activity: Start, Message, End.
Any task or class with a reference can log a message on a project.
Since a Java process only has one standard input, output, and error stream, only a single Build Listener can be dedicated as the logger for the Ant build. This means that build listeners that interact with the standard IO facilities have to have an interface that works with this. This is the BuildLogger interface.
There is a long list of provided listeners and loggers, from which you can choose. If you are intending to post-process logging output, some logger responds to a setting called Emacs Mode, which improves the ability of parsing tools to find file names and other structured information.
Calling from Java: Main Methods and Isolation
The Main Entrypoint and Core Loader: Calling from Java and IDEs
Ant has a command-line entry point, which can be extended and overridden. However, reusing it directly is a risky proposition, as its exit() method actually calls System.exit()
and terminates the current JVM. A major surprise if you have not read the documentation. The recommended way to overcome this behavior according to the manual is to subclass the existing Main class:
xxxxxxxxxx
public class NoSystemExitMain extends org.apache.tools.ant.Main {
/**
* This operation is expected to call {@link System#exit(int)}, which
* is what the base version does.
* However, it is possible to do something else.
* @param exitCode code to exit with
*/
protected void exit(final int exitCode) {
// System.exit(exitCode);
}
The main class is programmatically dispatched using the startAnt() method. It takes command-line arguments, user properties, and a reference to a class loader called the core loader. The core loader is used to load system Ant tasks and for taskdefs
that don't specify an explicit path.
This means that from a class loading perspective Ant can be reasonably isolated from the rest of the JVM.
The Argument Processor: Parameter Injection
It would be possible to pass all data for the execution to Ant via user properties, but this would be quite a bulky and cumbersome invocation. For this purpose, Ant has a facility that extends the command line argument parsing dynamically.
Another scenario is checking for resources and other requirements before starting a potentially expensive build.
For these purposes, Ant offers the argument processor facility. An ArgumentProcessor is a command-line processor plugin. It is defined using a service-provider interface 'ArgumentProcessor.'
The implementation as a service provider means that all argument processor classes that are registered via metadata in their JAR and are found on the classpath, will be added to a list and executed.
The interaction of the Ant command-line with an argument processor is as follows:
- The processor is provided with the list of arguments and queried for every position whether it wants to handle it.
- When all arguments have been parsed, the Processor is provided the additional arguments it has identified previously. It can make its own adjustments or indicate that the build should not proceed.
- Before the project is being configured, but after all other preparation is done, the Processor is called another time.
- After the project has been configured, that Processor can make its own adjustments or indicate that the build cannot proceed.
The Launcher: Classpaths and Conventional Directories
The main entry-point we have described above only takes a single classpath as an argument. The way this classpath is assembled can obviously be quite meaningful.
Ant has a convention for the Library Directories it uses. For this purpose, Ant offers the Launcher, which is intended to handle the assembly in a defined way.
If we want to use the Launcher from within Java, we hit a bug, though. The Launcher has a command argument '-main <class>
' to override Ant's normal entry point. This means we can use a subclass as described in the main entry point section to avoid that the entry point class will shut down our calling VM. Unfortunately, the launcher itself also has this behavior and it is not possible to work around it, as described in issue 65148 — AntLauncher terminates enclosing VM. There is nothing you can do here short of copying the Launchers' code.
Dependency Management and Runtimes
We have described how antlibs are installed and how their classpaths are created and maintained, and we have pointed out that the underlying software engineering tools that the tasks and custom components use need to be supplied. But we have not described how this can be done in a manner that is not ad-hoc and reproducible.
The Ant manual has a manually maintained table of dependencies for each task. However, collecting dependencies is not very satisfying. These days, no non-trivial Java build will work without a component repository, be it a Maven repository like Maven Central, an Eclipse P2 repository like Eclipse Orbit, or an OSGi Bundle Repository like the Felix OBR. We sort of expect that the components we use in Ant will also resolve in this manner.
The Missing Link at the Top
Ant does not have an associated component manager, so it is necessary to obtain one and to ensure that there is a description for the corresponding libraries that are required. The following sections look at different archive managers. Since archive managers are a topic to themselves, we can only give a brief overview and some recipes here.
Java Main and Ivy
Apache Ivy is a stand-alone dependency manager. Until 2011 Gradle used Ivy internally before implementing its own dependency management solution. From the point of view of Ant, Ivy operates as a set of tasks.
There are two ways you can set up Ivy with Ant:
- Install Ivy prior to execution.
- Auto-install Ivy as part of the build.
Ivy Pre-Installed
This is good if you apply to build system images that are going to be reused across the enterprise, and if you see the Ivy and Ant combination as infrastructure. Here is a Dockerfile that combines Ant and ivy. If you are installing ivy yourself be sure to choose the full distribution: Ivy can only download files if certain dependencies are in place. You can obtain Ivy from the download site.
Auto-Installed
The ivy manual has an example of a build file that downloads Ant using the Get Task. And you can download a ready-made build file for this purpose.
Gradle AntRun
Using Ant from Gradle is very easy, given that Gradle has originally grown out of Ant. Gradle allows using Ant Tasks directly, including custom tasks.
Maven
Ant and Maven can be mutually embedded. The AntRun Plugin allows Maven to call Ant and use the classpaths in Maven and custom tasks. Conversely, the Resolver Ant Tasks contain a version of Maven to download library dependencies for Ant. Using Ant in Maven is probably only sensible if you have an existing Maven build that you are working with.
Eclipse AntRunner
Ant in Eclipse has an installation of the AntRunner that is part of its Ant platform support which has access to the platforms' classloaders, but isolates Ant's tasks to the run itself. Tasks can be contributed via extension points called antTasks, antTypes, and extraClasspathEntries. This means that Ant tasks and dependencies can be packaged as plugins that can be added to the platform. To run this eclipse-ant-hybrid from the command line Eclipse contains a special headless application.
Opinions expressed by DZone contributors are their own.
Comments