Jaunder

Post by mdorman

M
Michael Alan Dorman@mdorman

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.