Building RPMs in Hudson or Jenkins

Hudson and Jenkins offer a great way to automate just about any type of build you can imagine. Creating artifacts is a piece of cake, but what if you want to create full packages? On this one page I go through the why and the how of using Hudson and Jenkins for RPM creation. We'll use the command line way of building RPMs. Maven can also be used for this task; this is certainly worth investigating if you're already building around this technology, but it does reduce the flexibility somewhat.

Why Use RPMs in the First Place?

If you've arrived at this page, you're probably already using Hudon/Jenkins, and may have set up automated builds with these tools. You may be looking at using something like Puppet or Chef for automated deployment too, so why bother with RPMs in the first place when you could just deploy artifacts with Puppet or Chef, or even Maven or over SSH?

In our software engineering world, you're probably comfortable with the concept of loose coupling. With this philosophy you define the interfaces between components then you can develop the components separately. This makes your projects more manageable, and if there's one part you don't like it's easy to swap it out for another. It's a common enough concept and great if you're not sure how things are really going to end up. Who is these days?

Packaging allows us to bring loose coupling to the world of the deployment pipeline. An RPM package (or whatever package is suitable for your architecture) allows you to define exactly where all your artifacts should be deployed and what dependencies you have. It does not specify the method of deployment. This brings the advantages of loosely coupled architecture; especially important when you're starting to build a deployment pipeline. An interesting exercise is to think of your own deployment pipeline in terms of loose coupling. Sketch it out and see what it looks like; what are your interfaces?

Building RPMs with Hudson/Jenkins

Setting Up

Before you start, you'll need to install the package containing the rpmbuild command and its dependencies. On Redhat this package is called "rpm-build", on Debian and Ubuntu it's simply called "rpm". Best of luck if your Hudson service is on a Windows server!

It might be worth trying to build RPMs on the command line then moving the process into Hudson - it will be a whole lot quicker to try things out that way.

Now we have the packages we need to create some directories that rpmbuild expects, so run the following from your working directory:
mkdir BUILD RPMS SOURCES SPECS SRPMS
This creates the following directories:

  • BUILD is used as tempoary complie area
  • RPMS contains the binaries
  • SOURCES is for your source code
  • SPECS contains your spec file
  • SRPMS contains the source RPM built during the process.
Most of this directory structure and in fact the whole process seems to be organised around building code from source. We could do that, even if we're building WAR files with Maven, but let's not get carried away here - we wouldn't want to blow our loose coupling.

Creating the Package

To run an RPM build we need a spec file put in the SPECS directory and some artifacts that we put in the SOURCES directory. The spec file contains several things, more importantly:

  • List of files to deploy and where they should go
  • Dependencies
  • Any shell scripting to run before the deployment
  • Any shell scripting to run after the deployment

Further details on RPM spec files can be found at www.rpm.org

To set this up, take the spec file (listed below) and save it as SPECS/helloworld.spec. In addition we need something to actually deploy, so let's create a HelloWorld shell script in the SOURCES directory as follows:

echo "echo Hello World" > SOURCES/helloworld.sh

Now we have everything in the right place to create our test RPM. To do this run:

rpmbuild --define '_topdir '`pwd` -ba SPECS/helloworld.spec

Your new RPM, called helloworld.rpm should now be in the RPMS/noarch/ directory. It's that easy to create an RPM!

Example Spec File

Name            : helloworld
Summary         : Hello world application
Version         : 0.0.1
Release         : 0.1

Group           : Applications/File
License         : (c)Douglas Gibbons

BuildArch       : noarch
BuildRoot       : %{_tmppath}/%{name}-%{version}-root


# Use "Requires" for any dependencies, for example:
# Requires        : tomcat6

# Description gives information about the rpm package. This can be expanded up to multiple lines.
%description
Hello World app


# Prep is used to set up the environment for building the rpm package
# Expansion of source tar balls are done in this section
%prep

# Used to compile and to build the source
%build

# The installation. 
# We actually just put all our install files into a directory structure that mimics a server directory structure here
%install
rm -rf $RPM_BUILD_ROOT
install -d -m 755 $RPM_BUILD_ROOT/usr/bin/
cp ../SOURCES/helloworld.sh $RPM_BUILD_ROOT/usr/bin/.

# Contains a list of the files that are part of the package
# See useful directives such as attr here: http://www.rpm.org/max-rpm-snapshot/s1-rpm-specref-files-list-directives.html
%files
%attr(755, root, -) /usr/bin/helloworld.sh
/usr/bin/helloworld.sh

# Used to store any changes between versions
%changelog

Working with Hudson/Jenkins

Now we've created a basic RPM build job we can modify it to make use of some of the Hudson/Jenkins features. I'm going to assume you're comfortable with setting up a Free-style software job in Hudson.

Let's assume that we have a new Hudson job, and our helloworld.spec and helloworld.sh files are put in the working directory of our hudson build (maybe by a subversion check-out as part of the Hudson build job). We could then add an Execute Shell stage and use the following code to build our RPM:

# Clean up and create directories
for dir in BUILD RPMS SOURCES SPECS SRPMS
do
 [[ -d $dir ]] && rm -Rf $dir
  mkdir $dir
done

# Put our files in the right place
mv helloworld.sh SOURCES/.
mv helloworld.spec SPECS/.

# Create rpm in RPMS/noarch/
rpmbuild --define '_topdir '`pwd` -ba SPECS/helloworld.spec

We would probably want to add an "Archive Artifacts" phase to the build too, to archive "RPMS/noarch/*.rpm"

Improvements to our Hudson/Jenkins Build

A little bit of fiddling with the script and spec files will help you create different RPMs, maybe deployment of WAR files with dependencies on tomcat for example.

If you don't set good versioning of your RPMs, things are going to get very confusing further down the deployment pipeline. All is not lost. We can automatically set versioning in the RPMs. To do this we can pass "marcros" (just variables in this case) through to our SPEC file from the rpmbuild command.

An example would be to set the Hudson build number. These come through as the BUILD_NUMBER variable in scripts that are part of the "Execute Shell" stage. Simple add --define 'BUILD_NUMBER '$BUILD_NUMBER to your rpmbuild command and then change the Release line of your spec file to be Release : %{?BUILD_NUMBER}

You can use the same technique to add values of a parametised build. Also view the list of Environment Variables link below the Hudson Execute Shell stage.

I hope that gives you an idea of how to create RPMs. If you have any ideas on how to improve this page please contact me!