How To write a plugin or a buildtype for easyant
A build module in Easyant is a logical unit that provides additional pluggable functionality to your build set up. You may choose to use or ignore such a plugin when running the build. A build module is composed, in the least, of a ant file associated with a ivy specs file.
So let's write a Hello World plugin.
Generating plugin from a skeleton
First we need to create a plugin structure. To ease plugin development easyant came with a skeleton for plugins.> easyant skeleton:newpluginIt will then ask you a few questions
[input] The path where the skeleton project will be unzipped [.]That's all !
[input] Organisation name of YOUR project [org.apache.easyant.plugins]
org.mycompany
[input] Module name of YOUR project
myplugin
[input] Revision number of YOUR project [0.1]
We've a ready to use plugin structure.
|-- module.ivy
`-- src
|-- main
| `-- resources
| `-- myplugin.ant
`-- test
`-- antunit
`-- myplugin-test.xml
Ant file
The skeleton has generated the plugin main file in src/main/resources/[MYPLUGIN].ant<project name="org.mycompany;myplugin"By convention, projectname of the plugin should be formed like
xmlns:ivy="antlib:org.apache.ivy.ant"
xmlns:ea="antlib:org.apache.easyant">
<!-- Force compliance with easyant-core to 0.7 or higher -->
<!-- <ea:core-version requiredrevision="[0.7,+]" /> -->
<!-- Sample init target -->
<target name="myplugin:init">
<!-- you should remove this echo message -->
<echo level="debug">This is the init target of myplugin</echo>
</target>
</project>
[organisation]#[module]Example:
org.mycompany#myplugin
Understanding extension-point
Extension-points are plugins hooks. Plugins typically add low-level tasks to one or more extension-points. For example, a plugin can contribute to processing sources before compilation, you will in that case plug your own target to "abstract-compile:compile-ready" extension-point". This plugin hooks is defined in abstract-compile plugin".So we need to import this plugin and plug our own target on it.
<ea:plugin module="abstract-compile" revision="0.9"/>Less typically, a plugin can also define new extension-points for other plugins to use. We highly recommend in that case to create an "abstract" plugin defining common stuff and extension-points to limit coupling between plugins and make them more flexible.
<target name="myplugin:mytarget" extensionOf="abstract-compile:compile-ready">
...//your stuff here
</target>
In standard build types the project-lifecycle is defined by a plugin named phases-std. This plugin loads the default lifecycle containing a set of high level extensionPoints like compile,package.
It's build types responsability to import this plugin and and do the binding between targets and extension-points from this lifecycle.
Target Naming Conventions
By default, all targets should be prefix by the plugin name. In our example "init" target is prefixed by "myplugin".There is a conventional difference in the way public and private targets are named in Easyant. A public target is one that makes sense for the end user to be aware of, while a private target should be hidden from the end user.
Conventionally,
- a public target should always have an associated 'description' attribute.
- a private target should begin with a "-"
Example :
<target name="myglugin:helloworld" depends="myplugin:init" description="display an hello world">Whereas a private target name should begin with '-'.
<echo>hello world !</echo>
</target>
<target name="myplugin:hello" depends="myplugin:init,-check-username" description="display an hello to current user">
<echo mess="Hello ${username}"/>
</target>
Example :
<!-- this target initialize username property if it's not already set -->
<target name="-myplugin:check-username" unless="username">
<echo>You can also add a "-Dusername=YOU" on the commandline to display a more personal hello message</echo>
<property name="username" value="${user.name}"/>
</target>
Pre conditions
A build module should always check that a set of pre conditions is met.This can be done at the root level of your plugin or in a target. We encourage you to use a target for initialisation as you can control when it should be executed. If intialisation is done at the root level it will be executed when the plugin is loaded.
By convention, the initialisation target should be named ":init".
Pre conditions, including for example - checking the existence of a file or a directory, could be performed inside this target. Additionally, this target is a great place to do global initializations that are needed for the rest of the build. This could include a taskdef initialization.
Pre conditions can be performed by using parameter task.
Example :
<target name="myplugin:init">
<ea:parameter property="username" required="false" description="the username used to display en 'hello Username' by calling :hello target"/>
</target>
What should be documented
The following elements needs to be documented- public targets / extension points descriptions
- parameters (properties, resource collections, paths). For each parameter specify name, description, whether it is required, and optionally a default value. This should be done with parameter task
- expected environment (files in a directory, a server up and running, ...)
- results produced
Publishing your plugin
You can easily publish your plugin to an easyant repository using the standard phases publish-shared (for snapshot) or release> easyant publish-local
> easyant publish-shared
> easyant releaseBy default plugins are published to a repository named easyant-shared-modules stored in $USER_HOME/.easyant/repository/easyant-shared-modules/.
You can specify the repository name using one of the following property
- release.resolver
- snapshot.resolver
Note: Repository must exist in easyant ivy instance. See configure easyant ivy instance man page for more informations.
Using your plugin in your project
Considering that you published your plugin in a easyant repository, you could use it in your project.<ivy-module version="2.0" xmlns:ea="http://www.easyant.org">And now running
<info organisation="org.mycompany" module="myproject"
status="integration" revision="0.1">
<ea:build module="build-std-java" revision="0.2">
<ea:plugin organisation="org.mycompany" module="myplugin" revision="0.1"/>
</ea:build>
</info>
<publications>
<artifact name="myplugin" type="ant"/>
</publications>
</ivy-module>
> easyant -pWe should see myplugin's target.
Main targets:
...
myplugin:hello display an hello to current user
myplugin:helloworld display an hello world
...
Getting further
Adding additional files to your module
Sometimes, we need to have a .properties files related to a given plugin.Sometimes it could be an additional file (an .xsl file for example).
Before using it we must declare the new file in the plugin module descriptor.
Open the module.ivy at the root level of plugin structure.
<ivy-module version="2.0" xmlns:ea="http://www.easyant.org">Here we defined that our plugin is composed of 3 files :
<info organisation="org.mycompany" module="myplugin"
status="integration" revision="0.1">
<!-- here we use build-std-ant-plugin build type that provide everything we need for plugin development -->
<ea:build module="build-std-ant-plugin" revision="0.9"/>
</info>
<configurations>
<conf name="default" description="runtime dependencies artifact can be used with this conf"/>
<conf name="test" description="this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases."/>
<conf name="provided" description="this is much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive."/>
</configurations>
<publications>
<!--Defines the plugin main script -->
<artifact name="myplugin" type="ant"/>
<!--Defines a property file -->
<artifact name="myplugin" type="properties"/>
<artifact name="myfile" type="xsl"/>
</publications>
</ivy-module>
- myplugin.ant (if name argument is not specified the module name will be used)
- myplugin.properties
- myfile.xsl
Considering that a plugin must be generic and can be retrieved from different repository (filesystem, url, ftp, etc...) we should take care of how we reference those additional files in our script.
To avoid any problems due to repository layout configuration, easyant gives you gives you access to properties containing the absolute path of a declared artifact. Those properties are composed with the following syntax :
[organisation].[module].[artifact].[type].fileExample:
org.mycompany.myplugin.myfile.xsl.fileThe '.artifact' is optional when module name and artifact name are the same.
[organisation].[module].[type].fileExample:
org.mycompany#myplugin.properties.fileSo loading a property file could be easy as :
<property file="${org.mycompany#myplugin.properties.file}" />If you want to copy / use an additional file
<copy file="${org.mycompany.myplugin.myfile.xsl.file}" tofile="..."/>
Using third party libraries
Most of the time when we write plugins we want to use third party ant tasks.Declaring dependencies in module.ivy
First we need to declare the dependency in the plugin module.ivy.<ivy-module version="2.0" xmlns:ea="http://www.easyant.org">Here we depend on amazingAntTask and myOtherAntTask provided by foobar organisation.
<info organisation="org.mycompany" module="myplugin"
status="integration" revision="0.1">
<ea:build module="build-std-ant-plugin" revision="0.1"/>
</info>
<configurations>
<conf name="default" description="runtime dependencies artifact can be used with this conf"/>
<conf name="test" description="this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases."/>
<conf name="provided" description="this is much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive."/>
</configurations>
<publications>
<artifact name="myplugin" type="ant"/>
</publications>
<dependencies>
<!-- your plugin dependencies goes here -->
<dependency org="foobar" name="amazingAntTask" rev="4.4" conf="default->default" />
<dependency org="foobar" name="myOtherAntTask" rev="4.4" conf="default->default" />
</dependencies>
</ivy-module>
Using dependency in your plugin ant script?
Easyant automatically creates a classpath specific for each plugin, this classpath contains all the required dependency .jars.The classpath is named
[organisation]#[module].classpathExample:
org.mycompany#myplugin.classpathSince this classpath is auto-created you can use it to reference your taskdef.
<target name="myplugin:init">
...
<taskdef resource="amazingAntTask.properties" classpathref="org.mycompany#myplugin.classpath" />
<taskdef resource="anotherAntTask.properties" classpathref="org.mycompany#myplugin.classpath" />
</target>
Compatibilty with core revision
A module can be dependent on features available in Easyant core. As such, it is possible for a module to be functional with particular versions of Easyant only.Easyant provides a way for modules to explicitly specify their dependency on core revisions.
A module may use the ea:core-version task to specify such a dependency.
A task may depend on:
- static version (Example : 0.5)
- dynamic version (Example : latest.revision) even if we do not recommand to use it
- listed version (Example : (0.1,0.3,0.5) )
- range version (Example : [0.5,0.8] means from 0.5 to 0.8. Example2 : [0.5,+] means all version superior to 0.5)
<project name="org.mycompany;myplugin"
xmlns:ivy="antlib:org.apache.ivy.ant"
xmlns:ea="antlib:org.apache.easyant">
<!-- Force compliance with easyant-core to 0.7 or higher -->
<ea:core-version requiredrevision="[0.7,+]" />
<!-- Sample init target -->
<target name=":init">
<!-- you should remove this echo message -->
<echo level="debug">This is the init target of myplugin</echo>
</target>
</project>
Writing plugin test case
By default the skeleton has generated a antunit test file in src/test/antunit/[module]-test.ant.So in our case let's open "src/test/antunit/myplugin-test.xml"
<project name="org.mycompany;myplugin-test"Here we :
xmlns:au="antlib:org.apache.ant.antunit"
xmlns:ivy="antlib:org.apache.ivy.ant"
xmlns:ea="antlib:org.apache.easyant">
<!-- Import your plugin -->
<property name="target" value="target/test-antunit"/>
<!-- configure easyant in current project -->
<ea:configure-easyant-ivy-instance />
<!-- import our local plugin -->
<ea:import-test-module moduleIvy="../../../module.ivy" sourceDirectory="../../main/resources"/>
<!-- Defines a setUp / tearDown (before each test) that cleans the environment -->
<target name="clean" description="remove stale build artifacts before / after each test">
<delete dir="${basedir}" includeemptydirs="true">
<include name="**/target/**"/>
<include name="**/lib/**"/>
</delete>
</target>
<target name="setUp" depends="clean"/>
<target name="tearDown" depends="clean"/>
<!-- init test case -->
<target name="test-myplugin:init" depends="myplugin:init">
<au:assertLogContains level="debug" text="This is the init target of myplugin"/>
</target>
</project>
- configure easyant for test
- import the plugin
- define a generic tearDown, setUp method that cleans the target and lib directories.
- define a test case for the init target that check that the output log contains "This is the init target of myplugin"
Now we will write a test case for our "myplugin:helloworld" target.
<target name="test-helloworld" depends="myplugin:helloworld">Tests can be executed by running :
<au:assertLogContains text="hello world !"/>
</target>
> easyant testYou can access test-report at "target/antunit/html/index.html" or if you prefer the brut result "target/antunit/xml/TEST-src.test.antunit.myplugin-test_xml.xml".