Developing Ant Plugins for Maven 2.x

Warning

The documentation below assumes that you have updated your locally cached cached copy of the maven-plugin-plugin. To update your copy, you will need to include the -U option when you build your plugin project:

mvn -U clean install

The maven-plugin-plugin is responsible for reading plugin metadata in its various forms and writing a standard Maven plugin descriptor based on the input. It was designed to accommodate multiple plugin languages side by side, but its initial design was slightly flawed for plugin languages that don't include the metadata inline with the source (within the same file). Since the 2.0.1 release of Maven, the maven-plugin-plugin has contained revisions to handle this scenario. Since the API has changed (in a backward-compatible way), and since the Ant plugin support requires these changes be in place, you will see an AbstractMethodError if you try to build an Ant-based plugin using the old maven-plugin-plugin.

Introduction

The intent of this document is to help users learn to develop Maven plugins using Ant.

As of the 2.0.1 release, Maven supports Ant-driven plugins. These plugins allow the invocation of Ant targets (specified in scripts embedded in the plugin JAR) at specific points in the build lifecycle. They can also inject parameter values into the Ant project instances when a target is called.

Conventions

In this guide, we'll use the standard Maven directory structure for projects, to keep our POMs as simple as possible. It's important to note that this is only a standard layout, not a requirement. The important locations for our discussion are the following:

  /<project-root>
  |
  +- pom.xml
  |
  +- /src
  |  |
  |  +- /main
  |  |  |
  |  |  +- /scripts (source location for script-driven plugins)
  |  |  |  |
  ...

Getting Started

We'll start with the simplest of all possible plugins. This plugin takes no parameters, and will simply print a message out to the screen when invoked. This should familiarize the reader with the basics of mapping Ant build scripts to the Maven plugin framework. From there, we will gradually increase the complexity of our plugin, adding parameters, interacting with standard project locations, and binding to lifecycle phases. Finally, we'll see how a single Ant build script can be mapped to multiple Maven mojos within the same plugin.

Hello, World

Our first plugin will simply print "Hello, World" to the console.

The Build Script

The elemental Ant-driven mojo consists of a simple Ant build script, a mapping metadata file, and of course the plugin's POM. If our goal is to print "Hello, World", we might use an Ant build script that looks something like this:

hello.build.xml:
--------------------------------

<project>
  <target name="hello">
    <echo>Hello, World</echo>
  </target>
</project>
The Mapping Document

Once we've created this build script, we need to tell Maven how to use it as a plugin. This involves creating a mapping document. Note that where the build script was named hello.build.xml, the mapping document is named hello.mojos.xml. The naming of these files is very important, as this is how the plugin parser matches mapping documents to build scripts. It has the general form:

  • basename.build.xml - The Ant build script.
  • basename.mojos.xml - The corresponding mapping document.

A simple mapping document used to wire the above build script into Maven's plugin framework might look as follows:

hello.mojos.xml:
------------------------------

<pluginMetadata>
  <mojos>
    <mojo>
      <goal>hello</goal>
        
      <!-- this element refers to the Ant target we'll invoke -->
      <call>hello</call>
      <description>
        Say Hello, World.
      </description>
    </mojo>
  </mojos>
</pluginMetadata>
The POM

Now that we have the build script and mapping document, we're ready to build this plugin. However, in order to build, we need to provide a POM for our new plugin. As it turns out, the POM required for an Ant-driven plugin is fairly complex. This is because we have to configure the maven-plugin-plugin to use the Ant plugin parsing tools in addition to the defaults (such as the Java parsing tools). Our POM might look something like this:

pom.xml:
------------------------------

<project>
  <modelVersion>4.0.0</modelVersion>
    
  <groupId>org.myproject.plugins</groupId>
  <artifactId>hello-plugin</artifactId>
  <version>1.0-SNAPSHOT</version>
    
  <packaging>maven-plugin</packaging>
  
  <name>Hello Plugin</name>
  
  <dependencies>
    <dependency>
      <groupId>org.apache.maven</groupId>
      <artifactId>maven-script-ant</artifactId>
      <version>2.0.6</version>
    </dependency>
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <!-- NOTE: We don't need groupId if the plugin's groupId is
             org.apache.maven.plugins OR org.codehaus.mojo.
        -->
        <artifactId>maven-plugin-plugin</artifactId>
        <version>2.5</version>
        
        <!-- Add the Ant plugin tools -->
        <dependencies>
          <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-tools-ant</artifactId>
            <version>2.5</version>
          </dependency>
        </dependencies>
        
        <!-- Tell the plugin-plugin which prefix we will use.
             Later, we'll configure Maven to allow us to invoke this
             plugin using the "prefix:mojo" shorthand.
        -->
        <configuration>
          <goalPrefix>hello</goalPrefix>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
Build It and Run It

Once we have a POM for our new plugin, we can install it into the local repository just as we would any other Maven project:

mvn install

and invoke it like this:

mvn org.myproject.plugins:hello-plugin:hello

This should output the following:

[echo] Hello, World

Using prefix:mojo Invocation Syntax

Our new plugin works, but look at that command line... The next thing is to configure Maven so we can use the familiar prefix:mojo invocation syntax, and leave that verbose, fully-qualified mess behind.

As you know, Maven maps plugins to user-friendly prefixes. However, these prefixes might overlap; that is, multiple plugins may try to use the same prefix inadvertently. To avoid the obvious ambiguity associated with such a collision, Maven will search a predetermined list of plugin groupIds for a given prefix, with the first match winning. So, if we want to add our new plugin to this search, we need to configure the list of plugin groupIds.

Configuring Plugin-Prefix Searching

In order to reference our new plugin by prefix, we need to add its groupId to the <pluginGroups/> list in the settings.xml file. As you probably know, this file is usually found under $HOME/.m2. The added section to make our plugin's groupId searchable should look like this:

~/.m2/settings.xml:
------------------------------

<settings>
  .
  .
  .
  <pluginGroups>
    <pluginGroup>org.myproject.plugins</pluginGroup>
  </pluginGroups>
  .
  .
  .
</settings>

Run It

We can check that this worked by invoking our new mojo once again, this time using the prefix syntax:

mvn hello:hello

Adding Plugin Parameters

Now, suppose it's not enough that our plugin display static text to the console. Suppose we need it to display a greeting that is a little more personalized. We can easily add support for this by adding a name parameter. For good measure, we'll output the current project's name as well.

Change the Ant Script

The build script will have to change to output the new information:

hello.build.xml:
--------------------------------

<project>
  <target name="hello">
    <echo>Hello, ${name}. You're building project: ${projectName}</echo>
  </target>
</project>

Change the Mapping Document

Now that we have a build script which requires two new parameters, we have to tell the mapping document about them, so they will be injected into the Ant Project instance.

hello.mojos.xml:
------------------------------

<pluginMetadata>
  <mojos>
    <mojo>
      <goal>hello</goal>
      
      <!-- this element refers to the Ant target we'll invoke -->
      <call>hello</call>
      
      <requiresProject>true</requiresProject>
      
      <description>
        Say Hello, including the user's name, and print the project name to the console.
      </description>
      <parameters>
        <parameter>
          <name>name</name> 
          <property>name</property> 
          <required>true</required> 
          <expression>${name}</expression>
          <type>java.lang.String</type>
          <description>The name of the user to greet.</description>
        </parameter>
        
        <parameter>
          <name>projectName</name> 
          <property>projectName</property>
          <required>true</required>
          <readonly>true</readonly>
          <defaultValue>${project.name}</defaultValue>
          <type>java.lang.String</type>
          <description>The name of the project currently being built.</description>
        </parameter>
      </parameters>
    </mojo>
  </mojos>
</pluginMetadata>

You'll notice several differences from the old version of the mapping document. First, we've added requiresProject="true" to the mojo declaration. This tells Maven that our mojo requires a valid project before it can execute. In our case, we need a project so we can determine the correct projectName to use. Next, we've added two parameter declarations to our mojo mapping; one for name and another for projectName.

The name parameter declaration provides an expression attribute. This allows the user to specify -Dname=somename on the command line. Otherwise, the only way to configure this parameter would be through a <configuration/> element within the plugin specification in the user's POM. Note that this parameter is required to have a value before our mojo can execute.

The projectName parameter declaration provides two other interesting items. First, it specifies a defaultValue attribute, which specifies an expression to be evaluated against Maven's current build state in order to extract the parameter's value. Second, it specifies a readonly attribute, which means the user cannot directly configure this parameter - either via command line or configuration within the POM. It can only be modified by modifying the build state referenced in the defaultValue. In our case, the name element of the POM. Also note that this parameter is declared to be required before our mojo can execute.

Rebuild It and Run It

Now that we've modified our plugin, we have to rebuild it before running it again.

mvn clean install

Next, we should run the plugin again to verify that it's doing what we expect. However, before we can run it, we have some requirements to satisfy. First, we have to be sure we're executing in the context of a valid Maven POM...runnning in the plugin's own project directory should satisfy that requirement. Then, we have to satisfy the name requirement. We can do this directly through the command line. So, the resulting invocation of our plugin will look like this:

mvn -Dname=<your-name-here> hello:hello

or, in my case:

mvn -Dname=John hello:hello

This should output the following:

[echo] Hello, John. You're building project: Hello Plugin

Defining Multiple Mojos from One Build Script

If you're familiar with Ant, you're probably familiar with the common usage pattern of defining multiple build types within a single build script. For instance, you might have a build type for cleaning the project, another for producing the application JAR file, and yet another for producing the full distribution including Javadocs, etc.

The concept is pretty simple. Discrete chunks of the build process are separated into targets within the script. These targets can reference one another in order to make reuse within the build script possible.

These same concepts map pretty well to Maven, actually. However, instead of targets directly referencing one another, they would be bound to the appropriate phases of the build lifecycle. In this way, multiple Ant targets from the same build script can be reused piecemeal at different points in multiple build lifecycles (clean, site, and the main lifecycle are three examples).

This section will describe how to map multiple logical mojos onto different targets within the same Ant build script. It's also possible to reference targets from multiple build scripts, but we'll cover this later.

Two Targets, One Script

To test this, we'll split our echo statement into two targets. Then, we'll reference each as separate mojos in the build. The new script looks like this:

one-two.build.xml:
--------------------------------

<project>
  <target name="one">
    <echo>Hello, ${name}.</echo>
  </target>
  
  <target name="two">
    <echo>You're building project: ${projectName}</echo>
  </target>
</project>

Map the Mojos

Next, we'll add new mapping document to map these two new mojos:

one-two.mojos.xml:
------------------------------

<pluginMetadata>
  <mojos>
    <mojo>
      <goal>one</goal>
      
      <!-- this element refers to the Ant target we'll invoke -->
      <call>one</call>
      
      <description>
        Say Hello. Include the user's name.
      </description>
      <parameters>
        <parameter>
          <name>name</name> 
          <property>name</property> 
          <required>true</required> 
          <expression>${name}</expression>
          <type>java.lang.String</type>
          <description>The name of the user to greet.</description>
        </parameter>
      </parameters>
    </mojo>
        
    <mojo>
      <goal>two</goal>
      
      <!-- this element refers to the Ant target we'll invoke -->
      <call>two</call>
      <requiresProject>true</requiresProject>
      
      <description>
        Write the project name to the console.
      </description>
      <parameters>
        <parameter>
          <name>projectName</name> 
          <property>projectName</property>
          <required>true</required>
          <readonly>true</readonly>
          <defaultValue>${project.name}</defaultValue>
          <type>java.lang.String</type>
          <description>The name of the project currently being built.</description>
        </parameter>
      </parameters>
    </mojo>
  </mojos>
</pluginMetadata>

Now that we've split the old functionality into two distinct mojos, there are some interesting consequences. Aside from the obvious, mojo one no longer requires a valid project instance in order to execute, since we only require the user's name in order to greet him.

Build It, Run It

Rebuild It and Run It

Since we've modified our plugin, we have to rebuild it again before re-running it.

mvn clean install

Now that we have two separate mojos, we can execute them singly, or in any order we choose. We can bind them to phases of the lifecycle using plugin configuration inside the build element of a POM. We can execute them like this:

mvn -Dname=John hello:one

RETURNS:

[echo] Hello, John.


mvn hello:two (executed in the plugin's project directory)

RETURNS:

[echo] You're building project: Hello Plugin

Alternatively, you could build a POM like this:

test-project/pom.xml:
----------------------------

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.myproject.tests</groupId>
  <artifactId>hello-plugin-tests</artifactId>
  <version>1.0</version>
  
  <name>Test Project</name>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.myproject.plugins</groupId>
        <artifactId>hello-plugin</artifactId>
        <version>1.0-SNAPSHOT</version>
        
        <configuration>
          <name>John</name>
        </configuration>
        
        <executions>
          <execution>
            <phase>validate</phase>
            <goals>
              <goal>one</goal>
              <goal>two</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Then, simply call Maven on this new POM:

cd test-project
mvn validate

You should see the following output:

[echo] Hello, John.
...
[echo] You're building project: Test Project

A Note on Multiple Build Scripts

It's worth mentioning that Ant-driven plugins can just as easily contain multiple Ant build scripts. Simply follow the naming rules - naming each A.build.xml, B.build.xml, C.build.xml, etc. for example - and be sure to provide a mapping document to correspond to each build script that contains a mojo (other build scripts may be contained in the plugin, and referenced by one of these; they don't need mapping documents). So, for the above examples (assuming they all contained mojo targets), you'd need: A.mojos.xml, B.mojos.xml, and C.mojos.xml. If C.build.xml was referenced by A and B, but didn't contain mojo targets, then you don't need a C.mojos.xml for obvious reasons.

Advanced Usage

Below are some tips on some of the more advanced options related to Ant mojos.

Component References

If your plugin needs a reference to a Plexus component, it will have to define something similar to the following in the mapping document:

<pluginMetadata>
  <mojos>
    <mojo>
      .
      .
      .
      <components>
        <component>
          <role>org.apache.maven.project.MavenProjectBuilder</role>
          <hint>default</hint> <!-- This is optional -->
        </component>
      </components>
      .
      .
      .
    </mojo>
  </mojos>
</pluginMetadata>

Forking New Lifecycles

In case your plugin needs to fork a new lifecycle, you can include the following in the mapping document:

<pluginMetadata>
  <mojos>
    <mojo>
      .
      .
      .
      <execute>
        <lifecycle>my-custom-lifecycle</lifecycle>
        <phase>package</phase>
        
        <!-- OR -->
        
        <goal>some:goal</goal>
      </execute>
      .
      .
      .
    </mojo>
  </mojos>
</pluginMetadata>

Deprecation

As time goes on, you will likely have to deprecate part of your plugin. Whether it's a mojo parameter, or even an entire mojo, Maven can support it, and remind your users that the mojo or configuration they're using is deprecated, and print a message directing them to adjust their usage.

To deprecate a mojo parameter, simply add this:

<pluginMetadata>
  <mojos>
    <mojo>
      .
      .
      .
      <parameters>
        <parameter>
          .
          .
          .
          <deprecated>Use this other parameter instead.</deprecated>
          .
          .
          .
        </parameter>
      </parameters>
      .
      .
      .
    </mojo>
  </mojos>
</pluginMetadata>

To deprecate an entire mojo, add this:

<pluginMetadata>
  <mojos>
    <mojo>
      .
      .
      .
      <deprecated>Use this other mojo instead.</deprecated>
      .
      .
      .
    </mojo>
  </mojos>
</pluginMetadata>