CVS Access and Logins
Ant supports several version-control systems, either natively or through the use of optional tasks. The version-control system called Concurrent Versions System (CVS) is used by Ant's developers to manage the Ant source code and is the version-control system that is the most integrated with Ant. CVS is widely used in both open-source and commercial software development, and it is available on most platforms. Although we focus on CVS here, the concepts demonstrated in this book can be applied to almost any revision-control system.
CVS Login
CVS has a repository that contains the revision history of each of the files. The location of this repository typically is referred to as CVSROOT. Usually, an environment variable named CVSROOT is set, and CVS commands implicitly make use of it to determine which repository to act on. The value of CVSROOT also can be passed as a parameter to CVS commands.
Most CVS tasks in Ant behave in the same manner as their CVS command-line counterparts. If the CVSROOT environment variable is set, the Ant tasks pick it up automatically. CVSROOT also can be set as an attribute on the CVS tasks. The one exception is the <cvslogin> task, which requires CVSROOT to be set as an attribute because it ignores the environment variable.
From a user's viewpoint, logging into CVS is not a step that takes place every time the developer accesses CVS. When a user logs in to CVS, a local password file, typically named .cvspass, is created. This file is used to authenticate the user on subsequent calls to CVS. So, the CVS login command doesn't actually log in the user, as most systems do, but it creates the password file in preparation for later CVS commands. Therefore, entering an invalid password when logging in does not do anything. The result is seen only when the first CVS command is run. Listing 3.18 shows the cvsLogin target.
Listing 3.18 cvsLogin Target
<!-- cvsLogin target --> <target name="cvsLogin"> <input message="Please enter CVS password:" addproperty="cvsPassword" /> <condition property="noCVSPassword"> <equals arg1="" arg2="${cvsPassword}" /> </condition> <fail if="noCVSPassword">You did not enter your CVS password.</fail> <cvspass cvsroot=":local:/usr/local/cvsArchive" password="${cvsPassword}" /> </target>
This target begins by incorporating the concepts from the backup tasks, where the <input> task was used to obtain the cvsPassword from the user. If no password is entered, the task fails with an appropriate error message.
Next we call the cvspass task to set the cvsroot and password attributes. The other optional attribute is passfile, which defaults to .cvspass if not overridden.
In reality, the cvsLogin target rarely is called because, in most installations of CVS, the login process is an operation that takes place once per user per machine. However, if users move from machine to machine, or if new users frequently are added, the frequency of logins could increase tremendously. Even if logging into CVS needs to happen more frequently, this step should occur naturally during the build process.
A Custom Task to Check for Required Input Parameters
Story
After a number of changes to the buildfile have been made to check that mandatory parameters are set, it becomes apparent to the developers that there is a lot of replication throughout the buildfile to check for required input parameters. Besides being sloppy, this replication creates a maintenance problem. The development team agrees that a pair of developers should write a custom task to clean this up and consolidate this functionality into one place.
The buildfile has grown to include a lot of constructs to verify that mandatory parameters are set. Recall from Chapter 2, "Creating Initial Spikes," that properties are immutable and can't be used like ordinary variables in a scripting language. Because of this, we wind up setting a number of temporary parameters to check for required input, and the buildfile becomes unwieldy. Also, whenever we want to require parameters in a buildfile, we wind up rewriting the same XML. The team decides to create a custom task that handles required input. This consolidates the checking of required input into a single task and also enables them to write and debug this code once. Listing 3.19 shows the custom task RequiredInput, which extends the Ant Input task. The Input task is not an abstract base class listed in the Ant documentation as a task specifically intended for extension, but there's also no reason why other tasks in Ant can't be extended.
A Task class in general has private attributes, public setter methods for each attribute, and a public execute method. When the task executes, the Ant framework calls the setter method for each attribute that is set in the XML tag. Then the public execute method is called to do the actual work. Appendix B, "Extending Ant," explains the general procedure for writing a custom task as well as methods for setting nested elements.
One of the problems faced in writing this task is that it's necessary to know the value of the attributes of the Input class, but they're private and the derived class doesn't have access to them. The way around this problem is to override the setter classes. Each setter method calls the base class's setter method (such as super.methodname()) and then stores the value of the attribute in the class. This grants access to a copy of the attribute values in the derived class.
When the RequiredInput task executes, it checks the boolean values to determine whether all the required attributes have been set. If they haven't, the value of the unpopulated attribute is false. The execute method then throws an Ant BuildException, which fails the build and prints an error message indicating which required parameter hasn't been set. This task is shown in Listing 3.19, which extends the Input task and simplifies the process of checking for mandatory parameters.
Listing 3.19 A Custom Task for Checking for Required Input
package com.networksByteDesign.util; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.taskdefs.Input; public class RequiredInput extends Input { /** * Defines the name of a property to be created from input. Behavior is * according to property task, which means that existing properties * cannot be overridden. * * @param addproperty Name for the property to be created from input */ public void setAddproperty (String addproperty) { super.setAddproperty(addproperty); propertyName = addproperty; havePropertyName = true; } /** * Returns the property that gets set during the build run. * @return The property being set. */ public String getProperty() { return propertyName; } /** * Sets the Error Message which gets displayed to the user during * the build run. * @param errorMessage The error message to be displayed. */ public void setErrormessage (String errorMessage) { this.errorMessage = errorMessage; haveErrorMessage = true; } /** * Returns the Error Message which gets displayed to the user during * the build run. * @return The error message to be displayed. */ public String getErrormessage() { return errorMessage; } /** * Actual test method executed by ant. * @exception BuildException */ public void execute () throws BuildException { if (!havePropertyName) { throw new BuildException("Missing attribute propertyName", location); } if (!haveErrorMessage) { throw new BuildException("Missing attribute errorMessage", location); } super.execute(); if(getProject().getProperty(propertyName).trim().length() == 0) { throw new BuildException(errorMessage, location); } } private String propertyName = ""; private String errorMessage = ""; private boolean haveErrorMessage = false; private boolean havePropertyName = false; }
Appendix B describes the integration of a custom task in detail. One method of hooking a custom task into an Ant buildfile is to declare a mapping between the classname and the taskname with a <taskdef> tag, as shown here:
<taskdef name="requiredInput" classname="com.networksByteDesign.util.RequiredInput"/>
Afterward, all that's required is to put the classfile in your system CLASSPATH, and then run the buildfile. There is an alternate, more "permanent" way to integrate a task, which is described in Appendix B.
Listing 3.20 shows the custom RequiredInput task hooked into our cvsLogin target.
Listing 3.20 RequiredInput Task Hooked into Build File
<?xml version="1.0" ?> <project name="eMarket" default="compile" basedir="."> <taskdef name="requiredInput" classname="com.networksByteDesign.util.RequiredInput"/> <property name="dirs.source" value="/usr/projects/eMarket/src" /> <property name="dirs.backup" value="${user.home}/backup" /> <property name="dirs.temp" value="/tmp" /> <!-- cvsLogin target --> <target name="cvsLogin"> <requiredInput message="Please enter CVS password:" addproperty="cvsPassword" errorMessage=" You didn't enter your CVS password."/> <cvspass cvsroot=":local:/usr/local/cvsArchive" password="${cvsPassword}" /> </target> <!-- compile target --> <target name="compile" description="Compile all of the source code."> <javac srcdir="${dirs.source}" /> </target> </project>
CVS Initialization
Now that we have refactored our Ant buildfile to deal with required input tasks, let's make the CVS login process transparent to the user. Because the CVS login is typically a rarely used target, we'd like to make sure it handles the login when it is necessary but doesn't get in the way of normal usage.
To accomplish this, we'll take a look at the <available> task. The purpose of the <available> task is to allow a specified property to be set if a particular resource, such as a file, a class, a directory, or a JVM system resource, exists. In this case, you can make the CVS login process transparent by looking for the .cvspass file. If it exists, you proceed normally. Otherwise, you prompt the user for the CVS password and create the .cvspass file. Listing 3.21 shows the cvsInit target.
Listing 3.21 cvsInit Target
<!-- cvsInit target --> <target name="cvsInit"> <available file="${user.home}/.cvspass" property="cvsAlreadyLoggedIn" /> <antcall target="cvsLogin" /> </target>
Next we modify the cvsLogin task to check for the cvsAlreadyLoggedIn property. Instead of using the if attribute as before, we use the unless attribute, which has the opposite effect. These changes are shown in Listing 3.22.
Listing 3.22 cvsLogin Target (Revised)
<!-- cvsLogin target --> <target name="cvsLogin" unless="cvsAlreadyLoggedIn"> <requiredInput message="Please enter CVS password:" addproperty="cvsPassword" errorMessage=" You didn't enter your CVS password."/> <cvspass cvsroot=":local:/usr/local/cvsArchive" password="${cvsPassword}" /> </target>
Now we can make all CVS targets depend on cvsInit, which ensures that the user is logged in if he has not previously done so. If a user already has logged in, the cvsLogin task will be bypassed.
The CVS Task
Now that you have handled the initial access into CVS, you can turn your attention to the <cvs> task itself. In its simplest form, the <cvs> task is simply a pass-through mechanism to the cvs command itself. All the CVS commands and options can run through the <cvs> task as well. Listing 3.23 shows the output from calling cvs --help-command. For additional information about CVS and a list of CVS commands and options, see http://www.cvshome.org/docs/manual/.
Installation of CVS
To install CVS, follow these steps:
Download CVS from http://www.cvshome.org
CVS is available for a number of different platforms. Be sure to check the installation instructions for your specific platform.
The CVS manual can be found at: http://www.cvshome.org/docs/manual
Listing 3.23 Output from the cvs --help-command
% cvs --help-command CVS commands are: add Add a new file/directory to the repository admin Administration front end for rcs annotate Show last revision where each line was modified checkout Checkout sources for editing commit Check files into the repository diff Show differences between revisions edit Get ready to edit a watched file editors See who is editing a watched file export Export sources from CVS, similar to checkout history Show repository access history import Import sources into CVS, using vendor branches init Create a CVS repository if it doesn't exist log Print out history information for files login Prompt for password for authenticating server. logout Removes entry in .cvspass for remote repository. rdiff Create 'patch' format diffs between releases release Indicate that a Module is no longer in use remove Remove an entry from the repository rtag Add a symbolic tag to a module status Display status information on checked out files tag Add a symbolic tag to checked out version of files unedit Undo an edit command update Bring work tree in sync with repository watch Set watches watchers See who is watching a file (Specify the --help option for a list of other help options)
CVS Checkouts
The first command we'll look at is the CVS checkout command. This command is used to retrieve a set of files from CVS into a new workspace. As with all CVS commands, you can configure a multitude of options and parameters to meet your specific needs. This book is not intended to be a tutorial on CVS, so we simply pick from some of the more common sets of options.
One of the advantages of using Ant for your build and deployment process is that you can easily check Ant buildfiles into version control, and you can track revisions of your buildfile as well. On many of our projects, the Ant buildfile had more revisions than the source code as we enhanced our build and deployment process throughout the lifecycle of the project. In fact, the buildfiles used as examples throughout this book were maintained in a CVS repository. The issue with keeping the Ant buildfile under version control is that it creates a circular dependency. To run a target on an Ant buildfile, you first must check the workspace out of version control. When the workspace has been checked out, the need for an Ant target to check out a new workspace is negated.
However, if the Ant file is contained in a separate workspace from the rest of the source code, a checkout target for pulling a particular release of the source code makes perfect sense. You will apply this technique later in the book when application deployment is discussed. For example, we have an application that has its installation CD built with Ant. The Ant buildfile checks out a copy of Tomcat and JBoss from CVS and includes it as part of the installation.
If you are familiar with other revision-control systems but not with CVS, there is an important distinction in the concept of checkout. In revision-control systems such as the Revision Control System (RCS), the Source Code Control System (SCCS), and Clearcase, checking out a file usually means retrieving a copy of it with the intent of modifying it. This means that the file is locked in the repository, preventing others from modifying the file until the person who locked the file releases the lock. By contrast, in CVS, checking out means pulling a copy of the source code, but not necessarily with the intent to modify any of it. Also, the checkout command can be used to pull code based on some criteria such as a tag, all the latest versions on the main branch, and so on. The important distinction here between CVS and other revision-control systems is that the term checkout doesn't mean that the repository is locked for modification, as it implies in most other revision-control systems.
The concepts presented so far will assist you in building robust, reusable targets that can be used interactively by a developer, but also as part of an unattended build and deployment process.
Listing 3.24 shows the cvsCheckout Target. This target depends on cvsInit, which ensures that the user has previously logged in to CVS, either through the Ant rules or through CVS directly.
Listing 3.24 cvsCheckout Target
<!-- cvsCheckout target --> <target name="cvsCheckout" depends="cvsInit"> <requiredInput message="Please enter CVS module:" addproperty="cvsModule" errorMessage="You didn't enter a CVS module." /> <requiredInput message="Please enter working directory:" addproperty="dirs.working" errorMessage="You didn't enter a working directory"/> <mkdir dir="${dirs.working}" /> <cvs package="${cvsModule}" dest="${dirs.working}" /> </target>
As with the backup targets, the user is prompted to provide the name of the module to check out from CVS, as well as the directory in which to place the checked-out code. Modules are a convenient way in CVS to group files and directories that are logically related. If the module or working directory is not properly entered, the target fails.
<mkdir dir="${dirs.working}" />
We then create the working directory in case it does not exist. As you learned earlier, if it does exist, the default behavior is to do nothing.
<cvs package="${cvsModule}" dest="${dirs.working}" />
Next, call the cvs task, passing the module that you want to check out in the package attribute and the working directory in the dest attribute. The default command for the <cvs> target, if no command is supplied, is to check out a new workspace; for the purposes of this discussion, you do not need to be concerned with the cvs options passed to the checkout command.
The end result of the cvsCheckout target is a checked-out CVS module in the directory of your choice. As with the backup process, you can break the interactive part of cvsCheckout into a separate target so that you can produce an unattended version as well.
CVS Updates and Commits
Now that we have checked out a version of the source code, we need a way to pull the latest changes that other developers on the team are checking in. In CVS, this is done through the use of the update command. As with the checkout command, all CVS commands and options can be used in the command attribute of the cvs task. We must add the attribute to the new target because we relied on the default value for the checkout target.
The following code shows the cvsUpdate target. The module name is not required because CVS looks in the ${dirs.source} to find the information it needs regarding the CVSROOT and the module where the source code is mastered. The update command displays a list of files that have been changed, updated, or added since the last update. For example, a ? in front of the filename indicates that this is a new file, an M indicates a modified file, and a C indicates that changes you have made conflict with other changes made to the same file and that CVS cannot resolve the conflicts automatically. Information on the meaning of each of the flags, as well as how to work with issues such as conflicts, can be found on the CVS Web site.
<!-- cvsUpdate target --> <target name="cvsUpdate" depends="cvsInit"> <cvs command="update" dest="${dirs.source}" /> </target>
In CVS, putting your changes into the repository is done with the commit command. This is analogous to the concept of checkin with other revision-control systems. Committing changes is straightforward. CVS prompts you to enter a comment describing the changes, which is always a good practice to follow. CVS attempts to use your default editor for the entering of the comments (the value of the EDITOR environment variable in UNIX, for example). The following code shows the Ant task for committing changes to CVS.
<!-- cvsCommit target --> <target name="cvsCommit" depends="cvsInit"> <cvs command="commit" dest="${dirs.source}" /> </target>
If you would rather use the <input> task for adding comments as you have been doing, just add the appropriate tasks, as shown in Listing 3.25.
Listing 3.25 cvsCommit Target (Revised)
<!-- cvsCommit target --> <target name="cvsCommit" depends="cvsInit"> <requiredInput message="Please enter your CVS comment:" addproperty="cvsComment" errorMessage=" You did not enter a CVS comment."/> <cvs command="commit -m ${cvsComment}" dest="${dirs.source}" /> </target>
In this case, we are enforcing a team policy in the build process rather than relying only on verbal and written policies and procedures. If the developer does not enter a cvs comment, the commit process will abort. If a comment is added, commit is called, with the m flag passing the comment as a parameter.
The cvsUpdate and cvsCommit targets work fine until two developers change the same file. Even if the changes are not in conflict, if one developer does not call cvsUpdate before cvsCommit, cvsCommit will fail. Although the error message will indicate what went wrong, it is better to change cvsCommit to be dependent on cvsUpdate.
<target name="cvsCommit" depends="cvsUpdate" description="">
If the changes are in conflict, the update will indicate this and the commit will fail with an appropriate error message. This rule will check in all your changes with the same comment. Depending on your team standards, this might not be appropriate. However, if you make small, logical changes to the source code, this approach can be a best practice. By concentrating on the reason for the change rather than the specific code changes, which can be easily gleaned from a comparison of the two versions, a reviewer of the comments can better understand the motivation for certain changes.