Catalyst and Hudson, Sitting in a Tree

December 18, 2009

Introduction

I’ve been developing Web applications for a long time. Over the past few years, I’ve become a fan of the Catalyst Web Framework. Much like Perl itself, it makes the easy things easy and the hard things possible. One of Catalyst’s biggest strengths is that it does not attempt to provide a complete package in the way that, for example, Rails does. Catalyst’s “bring your own modules” approach allows it to concentrate on the core concerns of a Web framework: requests and responses, URL dispatching, component interaction, session handling, security, etc. Even in those areas, Catalyst is well-designed and delegates responsibility to plugins.

But this article isn’t about Catalyst per se; it’s about automation. I’ve built quite a few Catalyst-based applications, and in another similarity to Perl, I’ve found that it’s quite possible to learn it in stages. The first projects got up and running quickly, but didn’t pay much attention to how Catalyst applications are built and deployed — it’s very easy to simply check out the source code, point Apache at it, and be off and running. Once deployed, there’s rarely time to go back and reexamine the fundamentals, so I’ve tried to use the opportunity afforded by starting new projects to learn. Each time I start from a clean slate, I resolve to do at least one thing better than last time: figuring out how to unit-test that tricky module, refactoring common code, fitting Rose::DB::Object into Catalyst’s Model layer. Recently, I’ve dug in to one of the more basic yet easily ignored facets of a Catalyst application: the build system.

The Plan

Having spent a year doing some Java development at work, I’ve become familiar with the “continuous integration” world. A lot of this stuff is the way I’ve always worked, since I’ve been on relatively small teams with decent source control practices and no need to work so independently that integration ever become an issue. I do think, however, that build automation is a highly worthwhile pursuit. When I think about build automation, a few principles come to mind:

  • One command for builds
  • Automated tests
  • Automated deployment if tests pass
  • Archived, versioned build artifacts

I’ve used CruiseControl and Hudson in the Java world, and vastly prefer Hudson. CruiseControl does the job, but it’s a huge pain to set up and administer compared to Hudson. Hudson also has an active plugin ecosystem. And it looks so good, you almost wouldn’t guess it’s Java…

So here’s what I want to accomplish: when code is checked in, Hudson automatically runs the build command, then it runs the tests, and if the tests pass, it packages the application and deploys it to a server where the latest bleeding-edge version is always available for access by QA.

PAR Boiled

The first issue that came up was that a build needs to produce “artifacts”, to borrow a Javaism. In a more traditional application, that’s the executable or installer or .tar.gz file. Up until now, I’d just been treating the directory full of source code as my application, relying on source control tags to keep track of version numbers and releases. But for automation it would be a lot better to produce a single file at the end, allowing me to take advantage of Hudson’s archiving and fingerprinting features. If that single file were also, effectively, an “executable”, it could even simplify deployment.

In the Java world, they have WAR files, which are essentially Web applications packaged in a standard format, inside a self-contained ZIP file. Java-based web servers know what to do when you hand them one of these files, making application deployment sim… well, nothing’s simple in the Java world, but at least the packaging is straightforward. And it turns out that this idea has been adapted for Perl, in the form of PAR.

It seemed like this was the solution to the artifact problem: the build should produce a PAR file at the end, which contains everything needed to run the application. I just ship that off to the web server and I’m done. And Catalyst even has support built in for packaging an application as a PAR file. Is it really that simple? Read on…

Cue Mr. Hudson

Hudson has a very straightforward hook to non-Ant build systems: a text box into which you enter shell commmands. That’s just fine with me. So “make catalyst_par” is all I need to do, right? Not exactly. I ran into a few problems, one of which the set of files chosen to put in the PAR file. Module::Install::Catalyst doesn’t provide much, if any control over the build process. It makes an attempt to do the right thing, but unfortunately it ends up including a lot of .svn directories, while missing some SQL files I had added. In the end, I settled on a somewhat awkward process of using “make distdir” to get a clean copy of the right files, changing into that directory, and continuing the build from there. I also had to relocate my SQL files to “lib” for now, which is a bit ugly. Here’s the guts of the script Hudson runs. I’ve highlighted two lines that I’ll be referring to in the following discussion.

[ -e Makefile ] && make realclean
perl Makefile.PL --skipdeps
make distdir
cd MyProj-*
cp lib/MyProj/DB.pm-dist lib/MyProj/DB.pm
cp myproj.conf-dist myproj.conf
echo "build hudson b$BUILD_NUMBER r$SVN_REVISION" >> myproj.conf
perl Makefile.PL
make
make testjunit
make catalyst_par
mv myproj.par ..
./script/db_upgrade.pl myproj_dev myproj
cp myproj.par /opt/myproj
/sbin/service myproj restart

Getting the Version Number

MyProj.pm has a version number. Module::Install helpfully finds that version number and uses it in the generated Makefile. It forms, for example, the directory name generated by “make distdir”. I wanted to add a target to the Makefile that would just print the version number, but I found that adding targets when using Module::Install is complicated and difficult. Thus began the arduous part of my journey. Of course, writing this after it’s all over, I know how to do it and I know there are plenty of other, possibly better ways to accomplish the same goal, so I’ll be returning to this script soon to fix it.

It Gets a Bit Messy

This may be worth reading at this point: http://stackoverflow.com/questions/73889/which-framework-should-i-use-to-write-modules. Catalyst used to generate Build.PL, but apparently it caused problems and now it’s been switched to using only Module::Install.

So now that we know how to build a PAR file, let’s back up a step and address testing. The Perl community, in my opinion, has high standards when it comes to testing. Catalyst makes sure to set up a test infrastructure for your application, and provides plenty of support for doing the kinds of things that could be tricky to test in a Web app. So testing is good, and we want to make sure to run our tests automatically. “make test” actually handles that perfectly well out of the box. But there’s an even better way.

Justin Mason has already been through the Hudson+Perl dance, and wrote a very nifty script to convert Perl’s test output to JUnit. The win here is that Hudson knows how to parse JUnit output, so instead of just “build failed”, now it can tell you which tests failed and where. I’ll take it!

At about the same time, I also got interested in Devel::Cover. I decided to start adding this stuff to the Makefile so that I wouldn’t have to maintain a lot of external scripts to run these natural parts of the build process. There’s a module on CPAN for including a Devel::Cover target in your Makefile. So I added it that first. Then I decided to add another target of my own to run the tests and do the JUnit conversion. This is where I found out that ExtUtils::MakeMaker only has one usable extension point — and it was taken. Also, excuse me not being much of a Makefile wizard.

# ExtUtils::MakeMaker::Coverage redefines MY::postamble
# This is a hack to be able to define a new target.
push @ExtUtils::MakeMaker::MM_Sections, "testjunit";
push @ExtUtils::MakeMaker::Overridable, "testjunit";
sub MY::testjunit {
  return <<END_TESTJUNIT
testjunit :
\t\$(MKPATH) testxml
\t-\$(MAKE) TEST_VERBOSE=1 test 2>&1 | tee make_test.log
\t\$(PERL) script/tap-to-junit-xml.pl --input make_test.log "make test" testxml/make_test
END_TESTJUNIT
}

makemaker_args clean => { FILES => [qw/make_test.log testxml/] };

There are two problems here. One is that I failed to see in the documentation for ExtUtils::MM::Coverage that there’s a much simpler way to call its postamble from your own. My mistake, which I’ll be fixing shortly. I’m showing my mistake here because I do think it illustrates just how messy MakeMaker is.

The second problem is that, in the end, is a small chunk of code. But getting to that small chunk of code took forever. I’ve already rambled on and on, so I’m not writing down all the false starts and dead ends I went through. In doing so, I read most of the code for Module::Install and MakeMaker. They’re almost the polar opposite of Catalyst, in that their behavior is hard-coded and not built to be extended. Certainly not in ways the original authors didn’t expect. It’s not that the code is necessarily bad, but I think a build system for the future should be more flexible. I haven’t looked at Module::Build much other than skimming the documentation, so maybe it’s not the perfect solution, but it looks like it would work better. Maybe I’ll end up porting this stuff to it. The problem is that Catalyst does provide some hooks for Module::Install, and I don’t have the time right now to reimplement things like the PAR building.

Conclusion

Continuous integration and build automation are worthwhile pursuits. I think the Perl community should pay more attention to tools like Hudson. Catalyst would benefit from improved build and deployment tools — something that the Rails community seems to be very actively working on these days. MakeMaker has got to do, and given that Module::Install is tightly coupled to it, I’m not sure it’s the right solution. In the end, I’m happy to have it all working, but I look forward to the next clean-slate opportunity when I’ll get a chance to do it again using what I’ve learned.

Leave a Reply