Sunday, January 13, 2008

Writing a Hudson plug-in (Part 1 – Preparation)

Writing a Hudson plug-in (Part 1 – Preparation)

This is the first part of a tutorial on writing a Hudson plug-in. The aim of this tutorial is to develop a Publisher for JavaNCSS. This publisher will work with both Hudson Freestyle projects and Hudson Maven2 projects. The publisher will also include a trend graph.

Before we can start plug-in development, we need to have a set of working projects to throw at our plugin. Ideally these projects should be simple and quick to build, but the most essential thing is that they build and run the tool that we want to publish the results of! We need a minimum of three projects:

  • Ant-based project
  • Maven2-based single module project
  • Maven2-based multiple module project

So we will develop these three basic projects, get them running the JavaNCSS tool, and then ensure they build in Hudson.

The Ant-based project

We’ll start with the Ant based project. First we’ll need a directory structure:

project/
src/
main/
test/
lib/
tools/
javancss/

Next we need to download the library dependencies to the lib folder (for example junit.jar) and the custom ant tasks to the appropriate sub-folder of tools.

You can get JUnit.jar from http://www.junit.org.

The JavaNCSS jars are available at http://www.kclee.de/clemens/java/javancss/ (Note this is the .de domain, not the .com domain). JavaNCSS comes as a zip file, so we’ll just extract the three jar files that we need: ccl.jar, javancss.jar and jhbasic.jar

At this point we should have the following

project/
src/
main/
test/
lib/
junit.jar
tools/
javancss/
ccl.jar
javancss.jar
jhbasic.jar

Next we need a build file, and some sample java files to build. Here is a sample build.xml that will do most of what we want:

<project name="SimpleAntProject" basedir="." default="dist">

<path id="javancss.ant.task">
<pathelement location="${basedir}/tools/javancss/ccl.jar"/>
<pathelement location="${basedir}/tools/javancss/javancss.jar"/>
<pathelement location="${basedir}/tools/javancss/jhbasic.jar"
</path>

<taskdef name="javancss"
classname="javancss.JavancssAntTask"
classpathref="javancss.ant.task"/>

<property name="src.dir" value="src"/>
<property name="java.dir" value="${src.dir}/main"/>
<property name="lib.dir" value="lib"/>
<property name="docs.dir" value="docs"/>
<property name="src.junit" value="${src.dir}/test"/>
<property name="manifest" value="${src.dir}/etc/manifest"/>
<property name="resource.dir" value="${src.dir}/resources"/>
<property name="build.dir" value="build"/>
<property name="dist.dir" value="dist"/>

<property name="build.classes" value="${build.dir}/classes"/>
<property name="build.lib" value="${build.dir}/lib"/>
<property name="build.javadocs" value="${build.dir}/javadocs"/>
<property name="build.tests" value="${build.dir}/testcases"/>
<property name="build.junit.reports" location="${build.tests}/reports"/>
<property name="manifest.tmp" value="${build.dir}/optional.manifest"/>

<!-- the absolute path -->
<property name="build.tests.value" location="${build.tests}"/>

<property name="debug" value="true"/>
<property name="deprecation" value="false"/>
<property name="optimize" value="true"/>
<property name="javac.target" value="1.5"/>
<property name="javac.source" value="1.5"/>
<property name="junit.fork" value="false"/>
<property name="junit.filtertrace" value="off"/>
<property name="junit.summary" value="no"/>
<property name="test.haltonfailure" value="false"/>
<property name="junit.forkmode" value="once"/>

<path id="classpath">
<pathelement location="${lib.dir}"/>
</path>

<path id="tests-classpath">
<pathelement location="${build.classes}"/>
<pathelement location="${build.tests}"/>
<pathelement location="${src.junit}"/>
<pathelement location="${lib.dir}/junit.jar"/>
<path refid="classpath"/>
</path>
<!-- turn this path into a string which is passed to the tests -->
<property name="tests-classpath.value" refid="tests-classpath"/>

<target name="clean" description="Delete all generated files">
<delete dir="${build.dir}" failonerror="false"/>
<delete dir="${dist.dir}" failonerror="false"/>
</target>

<target name="compile" description="Compiles ${ant.project.name}">
<mkdir dir="${build.classes}"/>
<javac srcdir="${java.dir}"
destdir="${build.classes}"
debug="${debug}"
target="${javac.target}"
source="${javac.source}"
deprecation="${deprecation}">
<classpath refid="classpath">
</javac>
</target>

<target name="compile-tests" depends="compile">
<mkdir dir="${build.tests}"></mkdir>
<javac srcdir="${src.junit}"
destdir="${build.tests}"
debug="${debug}"
target="${javac.target}"
source="${javac.source}"
deprecation="${deprecation}">
<classpath refid="tests-classpath"/>
</javac>
</target>

<target name="test" depends="compile-tests">
<mkdir dir="${build.junit.reports}"/>
<junit>
<classpath refid="tests-classpath"/>
<formatter type="brief" usefile="false"/>
<batchtest fork="yes"
todir="${build.junit.reports}"
haltonerror="true"
haltonfailure="true">
<fileset dir="${src.junit}"></fileset>
<include name="**/*Test*.java"></include>
<exclude name="**/AllTests.java"></exclude>
</batchtest>
</junit>
</target>

<target name="dist" description="Create the distribution" depends="compile, test, javancss">
<mkdir dir="${dist.dir}"/>
<jar destfile="${dist.dir}/${ant.project.name}.jar" basedir="${build.classes}"/>
</target>

<target name="javancss">
<javancss srcdir="${java.dir}"
generatereport="true"
outputfile="${build.dir}/javancss-report.xml"
format="xml"/>
</target>
</project>

We can then write some simple java classes and test our build. We’ll start with a simple HelloWorld.java

package com.onedash.hello;

/**
* A basic hello world application.
*
* @author Stephen Connolly
*/
public class Hello {
public String sayHello(String name) {
return "Hello " + name;
}

public static void main(String[] args) {
Hello instance = new Hello();
if (args.length == 1) {
System.out.println(instance.sayHello(args[0]));
} else {
System.out.println(instance.sayHello("world"));
}
}
}

At this point we should have the following project structure:

project/
build.xml
src/
main/
com/
onedash/
hello/
Hello.java
test/
lib/
junit.jar
tools/
javancss/
ccl.jar
javancss.jar
jhbasic.jar

And running ANT should give output like:

C:\local\project>ant clean dist
Buildfile: build.xml

clean:
[delete] Deleting directory C:\local\project\build
[delete] Deleting directory C:\local\project\dist

compile:
[mkdir] Created dir: C:\local\project\build\classes
[javac] Compiling 5 source files to C:\local\project\build\classes

compile-tests:
[mkdir] Created dir: C:\local\project\build\testcases
[javac] Compiling 1 source file to C:\local\project\build\testcases

test:
[mkdir] Created dir: C:\local\project\build\testcases\reports

javancss:
[javancss] Generating report

dist:
[mkdir] Created dir: C:\local\project\dist
[jar] Building jar: C:\local\project\dist\SimpleAntProject.jar

BUILD SUCCESSFUL
Total time: 3 seconds

And there should be a javancss-report.xml file in the build directory. That should be enough for an ANT project. Some extra finesse would be to add some more source code so we can examine the structure of the xml report.

The Maven2 based single module project

First we’ll need a directory structure:

project/
src/
main/
java/
test/
java/

Next we need a pom.xml to describe the project for Maven2. Here is a simple pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xsi="http://www.w3.org/2001/XMLSchema-instance">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- artifact identification -->

<groupId>com.onedash.hudson</groupId>
<artifactId>SimpleM2Project</artifactId>
<name>SimpleM2Project</name>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<!-- dependency specification -->

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.4</version>
<scope>test</scope>
</dependency>
</dependencies>

<!-- build process and details specification -->

<build>
<defaultGoal>package</defaultGoal>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>javancss-maven-plugin</artifactId>
<executions>
<execution>
<phase>process-sources</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

We can reuse the Hello.java from the Ant project, so at this point we should have the following directory structure:

project/
pom.xml
src/
main/
java/
com/
onedash/
hello/
Hello.java
test/
java/

And running maven should give the following output:

C:\local\project>mvn clean package
[INFO] Scanning for projects...
[INFO] ----------------------------------------------------------------------------
[INFO] Building SimpleM2Project
[INFO] task-segment: [clean, package]
[INFO] ----------------------------------------------------------------------------
[INFO] [clean:clean]
[INFO] Deleting directory C:\local\project\target
[INFO] Deleting directory C:\local\project\target\classes
[INFO] Deleting directory C:\local\project\target\test-classes
[INFO] Deleting directory C:\local\project\target\site
[INFO] [javancss:report {execution: default}]
[WARNING] Unable to locate Source XRef to link to - DISABLED
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 1 source files to C:\local\project\target\classes
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [surefire:test]
[INFO] No tests to run.
[INFO] [jar:jar]
[INFO] Building jar: C:\local\project\target\SimpleM2Project-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5 seconds
[INFO] Finished at: Sun Jan 13 22:06:20 GMT 2008
[INFO] Final Memory: 5M/10M
[INFO] ------------------------------------------------------------------------

And there should be a javancss-raw-report.xml file in the target directory.

The Maven2 based multi-module project

We can generate this project in its simplest form just by adding the simple project as a child of a project with <packaging>pom</packaging>. We’ll start off with the following directory structure:

project/
SimpleM2Project
pom.xml
src/
main/
java/
com/
onedash/
hello/
Hello.java
test/
java/

Where the SimpleM2Project subtree is a copy of the previous project. All we now need is a pom.xml to nest this project in:

<project xmlns="http://maven.apache.org/POM/4.0.0"
xsi="http://www.w3.org/2001/XMLSchema-instance"
schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- artifact identification -->

<groupId>com.onedash.mvn.hudson</groupId>
<artifactId>MultiM2Project</artifactId>
<name>MultiM2Project</name>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<!-- child modules -->
<modules>
<module>SimpleM2Project</module>
</modules>

</project>

And running maven should give the following output:

C:\local\project>mvn clean package
[INFO] Scanning for projects...
[INFO] Reactor build order:
[INFO] SimpleM2Project
[INFO] MultiM2Project
[INFO] ----------------------------------------------------------------------------
[INFO] Building SimpleM2Project
[INFO] task-segment: [clean, package]
[INFO] ----------------------------------------------------------------------------
[INFO] [clean:clean]
[INFO] Deleting directory C:\local\project\SimpleM2Project\target
[INFO] Deleting directory C:\local\project\SimpleM2Project\target\classes
[INFO] Deleting directory C:\local\project\SimpleM2Project\target\test-classes
[INFO] Deleting directory C:\local\project\SimpleM2Project\target\site
[INFO] [javancss:report {execution: default}]
[WARNING] Unable to locate Source XRef to link to - DISABLED
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 1 source files to C:\local\project\SimpleM2Project\target\classes
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] No sources to compile
[INFO] [surefire:test]
[INFO] No tests to run.
[INFO] [jar:jar]
[INFO] Building jar: C:\local\project\SimpleM2Project\target\SimpleM2Project-1.0-SNAPSHOT.jar
[INFO] ----------------------------------------------------------------------------
[INFO] Building MultiM2Project
[INFO] task-segment: [clean, package]
[INFO] ----------------------------------------------------------------------------
[INFO] [clean:clean]
[INFO] Deleting directory C:\local\project\target
[INFO] Deleting directory C:\local\project\target\classes
[INFO] Deleting directory C:\local\project\target\test-classes
[INFO] Deleting directory C:\local\project\target\site
[INFO] [site:attach-descriptor]
[INFO]
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] ------------------------------------------------------------------------
[INFO] SimpleM2Project ....................................... SUCCESS [7.240s]
[INFO] MultiM2Project ........................................ SUCCESS [2.754s]
[INFO] ------------------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10 seconds
[INFO] Finished at: Sun Jan 13 22:16:37 GMT 2008
[INFO] Final Memory: 7M/15M
[INFO] ------------------------------------------------------------------------

And there should be a javancss-raw-report.xml file in the SimpleM2Project/target directory.

The Hudson setup

We’ll start by running mvn hpi:create to create a base to work from:

C:\local>mvn hpi:create
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'hpi'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO] task-segment: [hpi:create] (aggregator-style)
[INFO] ----------------------------------------------------------------------------
...
[INFO] [hpi:create]
Enter the groupId of your plugin: org.jvnet.hudson.plugins
[INFO] Defaulting package to group ID: org.jvnet.hudson.plugins
Enter the artifactId of your plugin: javancss
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2 minutes 44 seconds
[INFO] Finished at: Sun Jan 13 22:34:58 GMT 2008
[INFO] Final Memory: 8M/15M
[INFO] ------------------------------------------------------------------------

The result should be a sub-directory called javancss with a pom.xml file and a basic Hudson plugin starting point. We’ll edit the pom.xml to update the version of Hudson to the latest (at the time of writing 1.170) and launch a live development instance to set our projects up in.

Open up the pom.xml file, and change the <version>1.153</version> tags for the hudson-core and hudson-war dependencies to 1.170.

The dependency section should now look like this:

<dependency>
<groupId>org.jvnet.hudson.main</groupId>
<artifactId>hudson-core</artifactId>
<version>1.170</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jvnet.hudson.main</groupId>
<artifactId>hudson-war</artifactId>
<type>war</type>
<version>1.170</version>
<scope>test</scope>
</dependency>

Next launch a copy of Hudson by running mvn hpi:run

C:\local\javancss>mvn hpi:run
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'hpi'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Unnamed - org.jvnet.hudson.plugins:javancss:hpi:1.0-SNAPSHOT
[INFO] task-segment: [hpi:run]
[INFO] ----------------------------------------------------------------------------
[INFO] Preparing hpi:run
...
[INFO] Started Jetty Server
[INFO] Starting scanner at interval of 1 seconds.

Now fire up a web browser for http://localhost:8080/

We want to create five projects:

  • Freestyle project for the Simple Ant project
  • Freestyle project for the Single module Maven2 project
  • Freestyle project for the Multi-module Maven2 project
  • Maven2 project for the Single module Maven2 project
  • Maven2 project for the Multi-module Maven2 project


For extra finesse, we can add a slave executor, and create a second set of these projects tied to the slave. An final extra project that does nothing other than trigger all the other projects to build is the icing on the cake.

6 comments:

Stephen Connolly said...

The sample projects are now available in the hudson CVS repository at /hudson/hudson/plugins/javancss/work/jobs/.../workspace/

Anonymous said...

the simple-m2-freestyle sample project are missing in the hudson plugin subversion

Rody Middelkoop said...

Dear Stephen,

You refer to the Hudson FishEye environment. (How) can I use the source code from the repository without having to download the raw-version of the files repeatedly? We are implementing Hudson in our company and the one plugin we miss is the JavaNCSS one.

sundu said...

once done with the plugin as said above what steps do i need to do to port it to other hudson(master) running on different machine

punit said...

Hi, nice post, just wanted to know, is there any difference in creating jenkins plugin by following your this post?
i mean can i follow this post to make a jenkins plugin, or this is specific to hudson only?

Thanks

Stephen Connolly said...

@punit:
No difference