Adventures in building Perl modules (a short, short primer on extending Module::Build)
Over the last few years, it has been a presumption that when I work on a
project in Perl, I will use the standard Perl tools--
ExtUtils::MakeMaker and, later, Module::Build --for managing the
Perl library code I write.
But yesterday, for the first time, I looked at extending Module::Build
to do more than just the stock actions. And you know what, it was easy.
Now the specific issue I was running up against was that I needed to insure that the database I was running my tests against was installed and clean. I had been using a Makefile, but that was a hack--for instance, I wasn't actually checking the presence of the database or anything, I was looking for a file I wrote when I created the database. I probably could have made make check for the actual database, but it's imperative, rather than procedural, style makes this kind of ugly.
Also, I wanted a clean instance of this database before I did a test run of the conversion utility (this is all work on a heavily revised AnteSpam, and we're moving from keeping config info in ldap to putting it in a replicated PostgreSQL database). And I wanted a clean instance of this database before I ran the PostgreSQL Autodoc tool to generate a nice diagram and DocBook documentation of the structure.
Oh, and I got so frigging tired of SQL's spectacular verbosity ( badly exacerbated by the fact that I was commenting on most of the structures so the information would show up in the DocBook documentation) that I wrote a simple preprocessor--so I had to make sure that was run if necessary before creating the database.
Oh, and I wanted to build the documentation automatically. And I kept
forgetting to run the Build script with the environment set properly
for the database, so I wanted that to be handled easily.
So, I made a file, Build.pm, which sits right alongside Build.PL,
and subclasses Module::Build. And to that file I added a function
(admittedly very simplistic, and, as a result, somewhat
overenthusiastic) to drop and recreate the database:
sub create_db {
my $self = shift;
# Get the database name
my $database = $self->args ("database");
# Drop the database if it already exists
$self->do_system (qq{dropdb $database}) if ($self->do_system (qq{psql -l | egrep -q $database}));
# Create the database
$self->do_system (qq{createdb $database});
# Make sure the schema is up-to-date
$self->dispatch ("ddlpp");
# Load up the schema and initial data
$self->do_system (qq{psql -q -f antespam.sql});
};
There are several cool things here. First, you can look at, at run-time, arguments that were given to the script when it was created. So you can do:
perl Build.PL database=foo
and when you actually invoke the resulting build script, the bits you
write can look for a database argument, and use what was set initially.
The rest of it should be fairly obvious--yes, I'm just shelling out to
psql rather than doing it all in DBI--except for the dispatch call. You
see, you can add additional actions to your script. In this case, I
added an action called ddlpp (for DDL pre-processor) to build the sql
from my data definition file. It's short, just:
sub ACTION_ddlpp {
my $self = shift;
$self->do_system (qq{ddlpp antespam.dp antespam.sql}) unless ($self->up_to_date ("antespam.dp", "antespam.sql"));
};
You'll notice, though, that it will only run ddlpp again if the .dp
file is newer than the .sql file. That's cool.
Anyway, I also overrode the standard test action, to make sure the
database is created:
sub ACTION_test {
my $self = shift;
# Set up database access
local $ENV{PGDATABASE} = $self->args ("database");
local $ENV{PGHOST} = $self->args ("host");
local $ENV{PGPASSWORD} = $self->args ("password");
local $ENV{PGUSER} = $self->args ("user");
# Make sure the database is created
$self->create_db;
# Run tests as normal
$self->SUPER::ACTION_test (@_);
}
All this does is set the appropriate environment variables for psql to
pick up, creates the database, and then runs the normal test action that
it inherited from Module::Build. The convert action is similar,
except it shells out to the convert script.
Etc., etc. I'm not holding this up as any paragon of implementation--in
fact, it's exposed some shortcuts I've taken that I ought not be taking,
so I'm gonna have to clean those up eventually, and I should be able to
add automatic .dp to .sql conversion and so forth--but for an hour
or two of poking around, I've made some not-inconsequential extensions
to the build system, giving me a much cleaner, more integrated process.