Lasse Koskela
Accenture Technology Solutions
Copyright © 2003 Lasse Koskela.
During the past couple of years I have more than once heard someone asking how to write an Ant build script which incorporates a certain degree of intelligence to be able to recognize the existence of a newly added J2EE module automatically -- without someone adding an entry into a .properties file or touching the build script itself. In fact, I was one of those someones.
For starters, let us state that it is often the case that we shouldn't embark on such a scripting mission. In the end, Ant is a build tool, not a scripting language. It is a completely valid argument that a build process should be explicit, which is not true when the build script incorporates such dynamic characteristics as detecting that a certain type of subdirectory should be processed using a different target than the next subdirectory. The intent is not to learn a particular scripting language but to learn how scripting languages in general can be employed within an Ant build script.
I wrote this short article because I wanted to know how to do it. I wanted to learn. Please, keep that in mind when the inevident sense of insanity penetrates your mind :)
You may have noticed that Ant provides a <script> task as part of the optional tasks. The <script> task can be used to execute any scripting language supported by the Bean Scripting Framework (BSF) from the Apache Jakarta Project (donated by IBM).
The supported scripting languages include the following:
I'll cover two of these languages, namely JavaScript and Jython, through a relatively simple example: dynamically calling a parameterized Ant target for each web application directory in our project's file system. I might come back and add example solutions for some other supported languages, and of course the reader is free to submit her own solutions for the community to see.
Although Ant's <script> tag supports all sorts of languages, each of them requires downloading one or more 3rd party libraries. The dependencies are listed in the Ant Manual's Library Dependencies page ("Ant Tasks" -> "Library Dependencies").
The most essential component is the BSF .jar file, which includes the implementation of the <script> task. In addition, we need some language specific libraries in order to be able to use JavaScript and Jython.
For JavaScript, we only need js.jar from the Mozilla Rhino project. Unzip the Rhino distribution .zip file and copy js.jar into your ANT_HOME/lib directory.
Jython, however, is a bit more complicated. First, we need to download a "self-extracting" .class file from Jython.org. Then, we need to install the Jython interpreter by calling
java jython-21on the downloaded .class file. This launches a graphical installer, which guides the user through installing the full Jython package somewhere on the user's computer. We're only interested in a single .jar file, jython.jar, which can be found under the installation directory. Again, copy jython.jar into your ANT_HOME/lib to make it available to the Ant runtime.
Your ANT_HOME/lib directory should now include at least the following .jar files:
optional.jar | Includes the script task definition (should come as part of the Ant distribution) | ||
bsf.jar | The scripting framework implementation | ||
js.jar | The implementation for JavaScript | ||
jython.jar | The implementation for Jython |
That should do it. Now, before we move on to the real thing, let's run a simple build script to verify that everything is set up correctly.
Copy-paste this build.xml somewhere and run it. If everything is in place, the outcome should include a nice greeting from each of the script blocks.
<?xml version="1.0"?> <project name="AntScriptingTest" default="test-all" basedir="."> <target name="test-all" depends="test-jython, test-javascript"/> <!-- - Runs an empty script to make sure that all necessary libraries are - available for Jython scripting. - - No, it's not a typo. The script task only recognizes Jython's old - name, "JPython". --> <target name="test-jython"> <script language="jpython"> <![CDATA[ import sys print "Hello from Jython!"; ]]> </script> </target> <!-- - Runs an empty script to make sure that all necessary libraries are - available for JavaScripting. --> <target name="test-javascript"> <script language="javascript"> <![CDATA[ importPackage(java.lang); System.out.println("Hello from JavaScript!"); ]]> </script> </target> </project>
It's cool to be able to run scripting languages from within an Ant build. However, in order to be actually useful, the script code has to integrate with the surrounding build script somehow.
The script can access two kinds of objects from the surrounding build script. First of all, any properties defined with the <property> tag that are visible to the <script> tag encapsulating a script can be accessed within the script. In other words, a property named "foo" in the build script is accessible as a variable named "foo" inside the script block.
Second, the <script> tag provides a bunch of implicit variables, namely project, self and any targets within the build script. project is a reference to the top-level object, the root element of the build script, <project>. self is a reference to the encapsulating <script> task. Finally, the implicit target references are named after the targets themselves. For example, a target named "foo" would be accessible for the script via the implicit variable "foo". We'll see how these implicit variables can be used later in the examples.
Regardless of the scripting language being used, the Ant API is something a developer needs to look at. Every Ant entity (project, target, task, and so on) is represented by a corresponding Java class in the Ant API. Also, the need for scripting the build script in the first place usually suggests that something has to happen dynamically, and that something has to do with building the project. This most definitely means that the script needs to manipulate the existing build script's structure by adding tasks, creating dependencies between targets, setting properties, etc.
Furthermore, many, if not all, of the supported scripting languages provide access to plain old Java classes. This is particularly useful if you need to communicate with, say, a 3rd party application server through their proprietary communications library. Yes, you could write your own Ant task for doing that, but it's often easier to do the same in a scripting language.
Let's assume that we have the following directory structure:
/src /main /web1 /WEB-INF /WEB-INF/web.xml /web2 /WEB-INF /WEB-INF/web.xml build.xml webapps-js.txt webapps-jy.txt
Yes, I know. It doesn't make much sense. Let's just accept it as it is and focus on the main subject -- the scripting part.
What we want to accomplish is that whenever the developers add a new web application directory into a certain location in the file system (say, adding "web3" next to "main", "web1", and "web2"), the build script wraps that new directory into a nice WAR archive without the developers touching the build script.
More specifically, we want to accomplish this by writing a small script that scans the file system for suitable directories, and then calls a parameterized Ant target which performs the WAR generation in a standard manner.
Let's start with the parameterized target our script is supposed to call:
<target name="genericwebapp"> <property name="webappFile" value="${pname}.war"/> <property name="webappRoot" value="src/${pname}"/> <delete file="${webappFile}" failonerror="false"/> <war destfile="${webappFile}" webxml="${webappRoot}/WEB-INF/web.xml" basedir="${webappRoot}" excludes="**/web.xml"/> </target>
The target expects a parameter named "pname" to indicate the name of the web application's root directory. For example, "web1". It then proceeds to make a WAR archive named "${pname}.war", accordingly.
Next, let's see how we end up running the scripted parts in the first place.
First, we need a top-level target that orchestrates the necessary sub-targets in order to get a happy ending. We'll have one such target for each scripting language we're going to use ("build_javascript" and "build_jython"). Second, we need a target to encapsulate the script block itself (again, one for each scripting language). Finally, we need some kind of a placeholder target ("build_generated") for the scripts to mess around with.
Below is an example of such a build script:
<?xml version="1.0"?> <project name="AntScripting" default="build_jython" basedir="."> <!-- - A top-level target for running our JavaScript implementation. --> <target name="build_javascript" depends="setup_javascript, build_generated" description="Performs a build using JavaScript for scripting"/> <!-- - A top-level target for running our Jython implementation. --> <target name="build_jython" depends="setup_jython, build_generated" description="Performs a build using Jython for scripting"/> <!-- - This target acts as a template to which the "build_setup_*" targets - dynamically add "antcall" tasks for calling the "genericwebapp" target - for each webapp directory. --> <target name="build_generated"/> <!-- - Dynamically creates "antcall" tasks into the "build_generated" target - based on the underlying directory structure using Jython. --> <target name="setup_jython"> <property name="webAppsFolder" value="src"/> <script language="jpython" src="webapps-jy.txt"/> </target> <!-- - Dynamically creates "antcall" tasks into the "build_generated" target - based on the underlying directory structure using JavaScript. --> <target name="setup_javascript"> <property name="webAppsFolder" value="src"/> <script language="javascript" src="webapps-js.txt"/> </target> <!-- - A generic target for generating a .war from a web application directory. --> <target name="genericwebapp"> <property name="webappFile" value="${pname}.war"/> <property name="webappRoot" value="src/${pname}"/> <delete file="${webappFile}" failonerror="false"/> <war destfile="${webappFile}" webxml="${webappRoot}/WEB-INF/web.xml" basedir="${webappRoot}" excludes="**/web.xml"/> </target> </project>
As you can see from the dependencies of our top-level targets, Ant will first execute the "setup_xxx" target encapsulating our script code, and then proceed to executing the "build_generated" target, which our script should've populated with appropriate tasks from within the "setup_xxx" target.
At this stage, it's probably good to introduce the two ways the <script> tag can be used.
The first way is to embed the actual script code inside the <script> element (encapsulated within a CDATA block) as illustrated by our test build.xml earlier. That's the simplest option as long as your script doesn't get too verbose.
The other option used in this larger example is to write the actual script code into a separate text file and have the <script> tag point to it using the "src" attribute. This is the best choice when the code grows to such proportions that it begins to make the build script itself difficult to read. Also, having the script in a separate file lets the developer write the scripts using the IDE of his choice, if there is one available.
Enough talking. Let's get down to the business.
Since we'll be using the file system and the Ant API, we need to do some importing. The Mozilla Rhino JavaScript engine supports the following kind of import syntax:
importPackage(java.lang, java.util, java.io); importPackage(Packages.org.apache.tools.ant); importPackage(Packages.org.apache.tools.ant.taskdefs);In other words, packages not within the standard java.* tree need to be prefixed with "Packages." in order to work. Individual classes can be imported with a similar syntax using the importClass() function.
After importing the needed Java libraries, we can access them just like in plain Java. For example, printing to standard output can be done with
System.out.println("Hello from JavaScript!");
Without further talk, here's the full source code for our JavaScript implementation. I have commented it with the intent of communicating what is happening.
importPackage(java.lang, java.util, java.io); importPackage(Packages.org.apache.tools.ant); importPackage(Packages.org.apache.tools.ant.taskdefs); // A "constant" for the file separator character var S = File.separator; // The main method (called from the bottom of the file). function main() { // "srcRoot" is the folder in which all web modules should reside var srcRoot = new File(System.getProperty("user.dir") + S + webAppsFolder); // Loop through all web modules and setup the antcall for each of them var iterator = findWebModules(srcRoot).iterator(); while (iterator.hasNext()) { addCallToGenericWebAppTarget(iterator.next()); } } // Returns a java.util.List of the directories under "srcRoot" containing a // web module. function findWebModules(srcRoot) { // "webModules" will contain the list of matching folders var webModules = new ArrayList(); // Loop through the directory contents var modules = srcRoot.list(); for (var i = 0; i < modules.length; i++) { var moduleDir = new File(srcRoot.getPath() + S + modules[i]); // If the sub directory looks like a web application, add it to the list if (isWebModule(moduleDir)) { webModules.add(moduleDir.getName()); } } return webModules; } // Determines whether the given directory contains a web module function isWebModule(directory) { var webXml = new File(directory + S + "WEB-INF" + S + "web.xml"); return webXml.exists(); } // Creates an "antcall" task for the "genericwebapp" target and configures // the parameters according to the given web module function addCallToGenericWebAppTarget(module) { // Create an "antcall" task which can be used for executing a target // within the same build script. Note the use of the implicit variable // "project"... var callTask = project.createTask("antcall"); // The target we want to call is "genericwebapp" callTask.setTarget("genericwebapp"); // Configure the parameters for the "antcall" task, namely the webapp // directory's name var param = callTask.createParam(); param.setName("pname"); param.setValue(module); // Add the created task into the body of the "build_generated" target. // Note the use of an implicit reference to the "build_generated" target... build_generated.addTask(callTask); System.out.println("added a call to genericwebapp for module " + module); } // Encapsulate everything nicely inside a main() method main();
Running the build.xml with "ant build_javascript" should produce an output similar to the following:
C:\AntScripting>ant build_javascript Buildfile: build.xml setup_javascript: [script] added a call to genericwebapp for module web1 [script] added a call to genericwebapp for module web2 build_generated: genericwebapp: [delete] Deleting: C:\AntScripting\web1.war [war] Building war: C:\AntScripting\web1.war genericwebapp: [delete] Deleting: C:\AntScripting\web2.war [war] Building war: C:\AntScripting\web2.war build_javascript: BUILD SUCCESSFUL Total time: 1 second
Again, the first thing to do in our Jython script is to import the needed libraries. The syntax for doing this is slightly different (Jython is basically "Python for Java"...) from JavaScript:
import java.util as util import java.lang as lang import java.io as ioWe have just imported the listed Java packages as Python modules into our script. From now on, we can refer to classes in those packages with the alias ("util" for "java.util" for example). In other words, java.util.ArrayList would be accessible using the notation "util.ArrayList".
Note that while I could've used Java's System.out.println() for writing into standard output, I've chosen to use the Jython print functionality which is less verbose, as is typical to scripting languages.
# 1) using Java's System.out.println() import java.lang as lang lang.System.out.println("Hello from Jython!"); # 2) using Jython's "native" print function print "Hello from Jython!";
So, let's see what our example looks like written in Jython:
import java.util as util import java.lang as lang import java.io as io ### A "constant" for the file separator character S = io.File.separator; ### the main() method represents the entry point into this script file def main(): srcDir = io.File(webAppsFolder); if srcDir.exists() != 1: raise Exception("Root folder '" + str(webAppsFolder) + "' does not exist"); # Loop through all web modules and setup the antcall for each of them webModules = findWebModules(srcDir); for i in range(webModules.size()): addCallToGenericWebAppTarget(webModules.get(i)); ### Retrieves a list of web module directories under the given root directory. def findWebModules(rootDir): list = util.ArrayList(); for subdir in rootDir.list(): # If the sub directory looks like a web application, add it to the list if isWebRoot(rootDir.getPath() + S + subdir): list.add(subdir); return list; ### Determines whether the given path represents a valid web application root. def isWebRoot(path): webxml = io.File(path + S + "WEB-INF" + S + "web.xml"); if webxml.exists(): return 1; else: return 0; ### Creates an "antcall" task into the "build_generated" target ### for the given webapp def addCallToGenericWebAppTarget(webModule): # Create an "antcall" task which can be used for executing a target within # the same build script. Note the use of the implicit variable "project"... antCallTask = project.createTask("antcall"); # The target we want to call is "genericwebapp" antCallTask.setTarget("genericwebapp"); # Configure the parameters for the antcall task, namely the webapp # directory's name param = antCallTask.createParam(); param.setName("pname"); param.setValue(webModule); # Add the created task into the body of the "build_generated" target. # Note the use of an implicit reference to the "build_generated" target... build_generated.addTask(antCallTask); print "added a call to genericwebapp for module " + str(webModule); ### Execute the main method which encapsulates all code in this script file main();
Running the build.xml with "ant build_jython" should produce an output similar to the following:
C:\AntScripting>ant build_jython Buildfile: build.xml setup_jython: [script] added a call to genericwebapp for module web1 [script] added a call to genericwebapp for module web2 build_generated: genericwebapp: [delete] Deleting: C:\AntScripting\web1.war [war] Building war: C:\AntScripting\web1.war genericwebapp: [delete] Deleting: C:\AntScripting\web2.war [war] Building war: C:\AntScripting\web2.war build_jython: BUILD SUCCESSFUL Total time: 2 seconds
We have just seen how a number of scripting languages can be embedded into an Ant build script with relative ease, yet packing enough power to actually accomplish something. In case you managed to get an urge to start scripting your builds right away, I've collected some useful links into the resources section.
Have fun!