Adventures in Dist::Zilla (among other things)

(Wow, has it really been almost 6 months since I last posted here?  Man, I’m slacking ...)

A while back, I decided to play with Dist::Zilla, and one of the first things I decided to do was make my own plugin bundle.1  Now, if you don’t know what a plugin bundle is ... well, that’s a bit above and beyond the scope of this article.2  Suffice it to say that, if you want to get the most of out DZ, you want to create your own plugin bundle.  (And, if you don’t want to do that, then you probably want to be using something simpler than DZ, like Dist::Milla or Minilla or Zilla::Dist.)

So I created one a long time ago but then I never did much with it.  I personally don’t have enough CPAN distros to juggle to make spending a lot of time fiddling with DZ a priority.  But lately I’ve decided I want to get back into it.  So I started out by installing the latest version I’d put out on CPAN.

Well, trying to install it, anyway.

When I did, cpanm told me it didn’t know what I was talking about.  WTF? I thought.  I checked MetaCPAN: my module was there.  I checked search.cpan.org: my module was there.  I checked PAUSE: my module was there.  So why doesn’t cpanm think it’s there?

Well, cpanm uses a file called ~/.cpanm/sources/http%www.cpan.org/02packages.details.txt to locate modules.  If your module isn’t there, it doesn’t really matter where else it is.  And my module wasn’t there.

So I was all set to fire off an email to the module-authors mailing list asking why PAUSE wasn’t indexing my module, but something was nagging me, in the back of my head ... hadn’t I read something in the Perl Weekly about this recently?  Why, yes ... yes, I had.3

A couple of months back, David Golden published this article explaining how PAUSE isn’t using your distname to figure out your module name any more.  I didn’t immediately think of this because David talked about a PAUSE permissions error, whereas what I was getting out of cpanm looked more like this:

[absalom:~] cpanm --info Dist::Zilla::PluginBundle::BAREFOOT
! Finding Dist::Zilla::PluginBundle::BAREFOOT on cpanmetadb failed.
! Finding Dist::Zilla::PluginBundle::BAREFOOT () on mirror http://www.cpan.org failed.
! Couldn't find module or a distribution Dist::Zilla::PluginBundle::BAREFOOT

But I wondered if this could be related, so I went back and reread the post to see what the root of the problem was.  It turns out that it shows up when you don’t have a package statement ... like when you use class instead.  Which I was doing.  For my little plugin bundle, I’m currently using MooseX::Declare.4  So, yes, I’m using class instead of package, so that’s probably my problem.

Now, I also remembered that David had done a follow-up article explaining how to fix it.  That post advises us to add a provides directive into our Makefile.  Unfortunately, that doesn’t help you much from the Dist::Zilla perspective, because the Makefile is generated for you.  What we need is a plugin to generate that for us, preferably by scanning our files and looking for class declarations.  And ... lo: there it is.  Dist::Zilla::Plugin::MetaProvides::Class.  So, if we’re using DZ directly, we would add this to our dist.ini:

[MetaProvides::Class]
inherit_version = 1

But of course we’re doing a plugin bundle, so we’ll do it like so:

$self->add_plugins (
    # lots of stuff here
 
    # metadata
    [ GithubMeta                =>  { remote => $self->git_remote } ],
    [ 'MetaProvides::Class'     =>  { inherit_version => 1 } ],          # <-- added this line
    MetaJSON                    =>
 
    # lots more stuff
);

And that’s pretty much all there is to it.

Except ...

As you may recall, I typically like to use TDD, where feasible.  Now, I hadn’t really done much in the way of unit tests for my plugin bundle ... as I said above, I never did too much with it, and it was primarily just a shameless rip-off of somebody else’s plugin bundle.5  But now I’d really like to figure out a way to test this new feature I want to add.  Of course, testing a Dist::Zilla plugin bundle isn’t the easiest thing in the world.  But I had an idea: I could create a temporary directory, create just enough fake files to make it into a fake distro, then run dzil build on it.  Then I can use the files that it builds to verify various things.

Let’s start with a basic test that should pass.  Here it is, built up bit by bit:

use Test::Most      0.25;

Our prefix.  Test::Most gives us strict and warnings, plus all the functions from Test::More and several other popular test modules.  This enables us to keep our boilerplate nice and simple.

use Path::Tiny qw< path cwd tempdir >;
 
our $tdist = tempdir( CLEANUP => 1 );
 
# go to our temp dir for this test
my $old = cwd;
END { chdir $old }              # go back to original dir so cleanup of temp dir can happen
chdir $tdist;

Instead of using File::Temp directly, I’m using Path::Tiny here, because it will make several other things easier as well.  We make a temp dir for our faux distro, and then chdir to it.  The one tricky bit is, if we’re still in the temp dir when our test ends, it can’t be cleaned up, because you can’t remove the current directory.  So I use an END block to make sure we go back wherever we came from.6

# create a skeletal distribution
 
my $tname = 'Test-Module';
my $tversion = '0.01';
 
path('dist.ini')->spew( <<END );
name                = $tname
author              = Buddy Burden <barefoot\@cpan.org>
license             = Artistic_2_0
copyright_holder    = Buddy Burden
 
version = $tversion
[\@BAREFOOT]
fake_release = 1
repository_link = none
END
 
my $lib = path( 'lib', 'Test' );
$lib->mkpath;
$lib->path('Module.pm')->spew( <<'END' );
package Test::Module;
 
# ABSTRACT: Just a module for testing
# VERSION
 
1;
END
 
my $t = path( 't' );
$t->mkpath;
$t->path('require.t')->spew( <<'END' );
use Test::Most 0.25;
 
require_ok( 'Test::Module' );
 
done_testing;
END

That’s about the minimum you can get away with for a DZ distro.  I add the fake_release bit just in case I want to test releasing someday ... I definitely don’t want my unit tests accidentally uploading things to PAUSE.  And I need the repository_link = none bit because I’m using the GithubMeta plugin, but I’m not creating a git repo for my fake distro.

# now build our test dist so we can have some files to test
 
demand_successful_command("dzil build");
chdir "$tname-$tversion";
 
my $meta = path('META.json')->slurp;
 
like $meta, qr/"version" \s* : \s* "$tversion"/x, 'version is correct in meta';

Run dzil build, change to the build dir it makes, suck in our META.json file, and verify something trivial in it (in this case, the version number).  Easy peasy.

done_testing;

Have to let the TAP harness know we successfully reached the end our test file without keeling over in the middle.

sub demand_successful_command
{
    my ($command) = @_;
 
    # get rid of stdout, but keep stderr
    # it might aid in debugging
    is system("$command >/dev/null"), 0, "command succeeded [$command]"
            or done_testing, exit;
}

And, finally, the definition of our function to run a command: execute, toss stdout, keep stderr, verify success, bail out if not.

(You can see the full version of this first cut at my unit test on GitHub.)

So, I ran this and it worked like a charm.  Now what I needed was a failing test.7  Well, at this point, that’s pretty simple.  We just change this:

my $lib = path( 'lib', 'Test' );
$lib->mkpath;
$lib->path('Module.pm')->spew( <<'END' );
package Test::Module;
 
# ABSTRACT: Just a module for testing
# VERSION
 
1;
END

to this:

my $lib = path( 'lib', 'Test' );
$lib->mkpath;
$lib->path('Module.pm')->spew( <<'END' );
class Test::Module with Some::Role
{
# ABSTRACT: Just a module for testing
# VERSION
}
 
1;
END

and add one line towards the end:

like $meta, qr/"provides" \s* : /x, 'contains a `provides` in meta';

And, voilà: verification of our META.json tweak.  (Again, full code for this version can be seen on GitHub.)  This code runs, the test fails, I add the line to my plugin bundle that I showed you before, test passes, and bob’s yer uncle.

Now, before I uploaded this to PAUSE, I decided to go ahead and build my distro and verify that the META.json would be correct now.  Except it wasn’t.  I spend an embarrasingly long time trying to figure out what the hell was going on there before I realized that my plugin module is self-hosting: that is, I build my DZ plugin module using my DZ plugin module.  This gives us a bit of a chicken-and-egg problem; specifically, when I build the new version of my module, I’m actually using an old version of my module ... whatever the last version I had installed was.  My new code is local to my git repo, but, when I build the module, it’s going out to the old code, installed wherever Perl modules are installed.8  Which is fine, most of the time.  But, in this case, building using the old code means that I’m not actually going to fix my problem: the META.json I’m about to upload to PAUSE doesn’t contain the provides directive, so it’s still not going to get indexed.  Crap.

We can fix this, though.  Build the new version locally but do not upload it to PAUSE.  Now use cpanm to install the resulting tarfile:

cpanm -n Dist-Zilla-PluginBundle-BAREFOOT-0.04.tar.gz

Now, build it again (which will now be using the new code), verify that the META.json looks right (it does), then upload to PAUSE.  Yay!

But now CPAN Testers lets us know that it hates us.  Sigh.

sh: 1: dzil: not found
 
#   Failed test 'command succeeded [dzil build]'
#   at t/tdist.t line 79.
#          got: '32512'
#     expected: '0'
# Looks like you failed 1 test of 1.

My first thought was, how can dzil not be found?  After all, Dist::Zilla is listed as a dependency of ours, so it has to be installed ... right?  Well, yes, but that doesn’t mean that dzil necessarily has to be in the path.  For some smokers, it will be.  For others, it won’t.  Counting on being able to find it is just not a good idea, in the general case.

Briefly I considered posting something to the perl-qa mailing list asking how I can figure out where the bin/ directory is.  Maybe I could use @INC to figure out where Dist::Zilla is installed, and somehow reverse engineer where dzil must be ... ?  But then a much simpler solution occurred to me.

Have you ever looked at the code for dzil?  It’s super-simple.  It’s just a wrapper around the run method of the application class.  So why bother to shell out to run an external command from our Perl unit test?  We can just run the application class ourselves.

Again, this is pretty simple.  In our unit test, we’ll need to add:

use Dist::Zilla::App;

And we’ll need to change this line:

demand_successful_command("dzil build");

to this:

run_dzil_command("build");

And define that command thusly:

sub run_dzil_command
{
    local @ARGV = @_;
    Dist::Zilla::App->run or die("failed to run dzil");
}

How simple can you get?  Dist::Zilla::App is expecting to find its arguments in @ARGV, so we’ll transfer @_ there, but we use local to make sure we only temporarily override the arguments to our unit test.9  Then we just run run and call it a day.

One problem though: apparently run doesn’t return success when it’s successful.  I didn’t spend any time trying to figure out why that was true; I just moved my or die somewhere else:

chdir "$tname-$tversion" or die("failed to run dzil");

There: now, if the build fails, we won’t get the build dir, therefore we can’t chdir to it, therefore failure to chdir indicates the build didn’t work.  Simple enough.  (Again, full version on GitHub.)

So, that was my recent adventure with Dist::Zilla.10  Hopefully there’s something in there that will be helpful to at least a few people.  I’d like to tell you that this solved all my problems, but the truth of the matter is it didn’t.  Maybe now that I’ve figured out how to tell PAUSE about my class, next time I’ll tell you how to tell Pod::Weaver about it.

As soon as I figure out how myself ...


1 Warning: Don’t actually try to install my plugin bundle—it won’t install for you.  Still working on that.  But I think the value of other people’s plugin bundles is not so much in the installing as in the code perusal anyway.


2 If you need a good place to start learning about Dist::Zilla in general, try the official tutorial.  Lots of good info there.


3 And this demonstrates (once again) why you too should be subscribed to the Perl Weekly.  Go sign up now, if for some reason you haven’t already.


4 But I’ll probably switch over to Moops at some point.


5 In this case, David Golden’s.  Which I suppose is an odd coincidence, given it was his article that led me to find the problem here.


6 Technically, it doesn’t matter where we go, since it only happens right before we exit, and the current directory won’t persist past the end of our process.  So I could have used chdir '..' or even chdir '/'.  But this seemed politer/safer/more obvious.  Although maybe that’s just me.


7 ‘Cause, you know: that’s how TDD works.


8 In this case, it’s the lib/ directory of a Perlbrew’ed verison of perl.


9 Likely that isn’t strictly necessary, but you never know.  It’s one of those things that will be sheer hell to track down if it ever does cause a problem, so why risk it?


10 And dzil plugin bundles, and PAUSE, and Perl Weekly, and TDD, and CPAN Testers ...


Leave a comment

About Buddy Burden

user-pic 14 years in California, 25 years in Perl, 34 years in computers, 55 years in bare feet.