Don’t Repeat Your… version number

Aristotle on 2006-07-20T23:27:55

The other day, Philippe Bruhat (AKA BooK) posted the following to the module-authors mailing list:

[Y]ou could also simply expose the information in the documentation, and fetch it from there: (a trick I discovered thanks to Abigail’s additions to Acme::MetaSyntactic, see the upcoming Acme::MetaSyntactic::tour_de_france for an example):

my %const = map  { s/\s+//; $_ }
            map  { split /\s*=>\s*/ }  
            grep { /=>/ }              
            map  { split /\n/ } << '=cut';

=pod

This module uses the following constants:
										
	bang_eth => 1                       
	biff     => 2
	krunch   => 3

=cut

Now, that in itself is a damn cool hack.

But it immediately set my mind thinking about how to use it for the one thing that always annoys me about module maintenance: updating the module version in both your POD and on the $VERSION line.

Turns out, this is actually very tricky because you have to get the polyglot understood by three different tools:

  1. perl, which uses a very simple rule for what it regards as non-POD. Easy – see above.

  2. POD formatters, which use even simpler rules for what they regard as POD. No problems here, and it’s what makes Abigail’s trick possible.

  3. ExtUtils::MakeMaker – or to be precise, its MM->parse_version method –, which is what a lot of modules use to extract version information from modules. Oh dear.

    It skips POD using… shall we say, simplistic rules, much like POD formatters, so it will tend to successfully ignore precisely the things that a POD formatter will accept. It will also accept only a single line, which will be eval’ed in isolation.

In other words, something like this, which was my first thought, won’t work:

$VERSION = ( <<'=cut' =~ /\b\d+\.\d+\b/ );

=head1 VERSION

This document describes Some::Module 0.1

=cut

If you try that, you will find that parse_version will eval just this:

$VERSION = ( <<'=cut' =~ /\b\d+\.\d+\b/ );

Useless.

I had to resort to treachery: reading MakeMaker’s source to find its weaknesses. And the weakness, it turns out, is that it uses /^=cut/ to stop skipping. Notice something? That matches too many things… Gotcha! You’re going down.

Unfortunately, the single-line requirement means that the version number must be on the same line as the string $VERSION, which means we’ll have to have that in the POD:

eval "package Some::Module; $_" for grep m/ = /, split /\n/, <<'=cut';

=head1 VERSION

=for fooling makemaker
=cut-feigned

This document describes Some::Module,
$VERSION = 0.1

=cut

Here, the heredoc operator on the first line sets perl up to treat the entire following section as a string. In that string we look for a line with an equals operator, then eval it.

The =for line makes POD formatters ignore what’s on the next line, unless one of them thinks it’s the formatter for the output format called “fooling”, which is unlikely to ever be written.

And what’s on the next line, the =cut-feigned, makes parse_version stop skipping and look for a line which sets $VERSION.

It works:

$ perldoc ./Some/Module.pm | grep VERSION
       This document describes Some::Module, $VERSION = 0.1
$ perl -MSome::Module \
       -le'print Some::Module->VERSION'
0.1
$ perl -MExtUtils::MakeMaker \
       -le'print MM->parse_version(shift)' Some/Module.pm
0.1

Would I use this is actual CPAN-published code? I don’t know. But you have to admit, it is really quite a fun hack.


CVS

Abigail on 2006-07-21T08:05:26

It's a cool hack, but I use something else to avoid having to modify the version number twice. I don't modify the version number at all - I let CVS do that for me:

our ($VERSION) = q $Revision: 1.0$ =~ /[\d.]+/g;

=head1 VERSION

$Revision: 1.0$
The previous line starts with a significant space, but <ecode> decided to ignore it.

=cut

And since I use a template to start a module with, I never have to remember to set the version number.

Not a cool hack, but something I do use for CPAN modules.

Re:CVS

Dom2 on 2006-07-21T11:25:58

The trouble with that is that you're letting CVS control your version numbers. That's not something I'm particularly comfortable with doing.

-Dom

Re:CVS

Aristotle on 2006-07-21T11:45:52

Yeah, over on PerlMonks, David Golden suggested something conceptually very similar, where he uses Pod::WikiDoc to handle POD that looks like this:

=begin wikidoc

This documentation refers to version %%VERSION%%.

=end wikidoc

And then he has a few custom rules in Build.PL as documented in Pod::WikiDoc::Cookbook, so that Some::Module->VERSION is substituted into place at ./Build dist time.

I like that approach a little better for the reason Dominic mentioned – I don’t want to leave version numbers to CVS.

M::B?

ChrisDolan on 2006-07-21T14:29:31

Have you tested this hack with Module::Build?

Re:M::B?

Aristotle on 2006-07-21T15:53:50

Nope. Does it fail?

Re:M::B?

ChrisDolan on 2006-07-23T13:18:45

I don't know, I was just wondering if you had tested it. IIUC, M::B uses it's own algorithm for $VERSION detect which emulates EU::MM, but is not identical.