File Coverage

blib/lib/Dist/Zilla/Plugin/PromptIfStale.pm
Criterion Covered Total %
statement 178 187 95.1
branch 78 104 75.0
condition 33 46 71.7
subroutine 32 33 96.9
pod 1 6 16.6
total 322 376 85.6


line stmt bran cond sub pod time code
1 27     27   26592777 use strict;
  27         81  
  27         850  
2 27     27   154 use warnings;
  27         56  
  27         1605  
3             package Dist::Zilla::Plugin::PromptIfStale; # git description: v0.056-4-gfae9b39
4             # vim: set ts=8 sts=2 sw=2 tw=115 et :
5             # ABSTRACT: Check at build/release time if modules are out of date
6             # KEYWORDS: prerequisites upstream dependencies modules metadata update stale
7              
8             our $VERSION = '0.057';
9              
10 27     27   5187 use Moose;
  27         3317295  
  27         209  
11             with 'Dist::Zilla::Role::BeforeBuild',
12             'Dist::Zilla::Role::AfterBuild',
13             'Dist::Zilla::Role::BeforeRelease';
14              
15 27     27   182116 use Moose::Util::TypeConstraints 'enum';
  27         68  
  27         257  
16 27     27   11486 use List::Util 1.45 qw(none any uniq);
  27         783  
  27         2504  
17 27     27   6248 use version;
  27         20656  
  27         222  
18 27     27   2323 use Moose::Util 'find_meta';
  27         66  
  27         214  
19 27     27   6004 use Path::Tiny;
  27         1781  
  27         1340  
20 27     27   15769 use CPAN::DistnameInfo;
  27         25475  
  27         924  
21 27     27   189 use HTTP::Tiny;
  27         61  
  27         604  
22 27     27   145 use YAML::Tiny;
  27         58  
  27         1653  
23 27     27   14932 use Module::Metadata 1.000023;
  27         151886  
  27         862  
24 27     27   8559 use Encode ();
  27         202382  
  27         714  
25 27     27   4273 use namespace::autoclean;
  27         74035  
  27         225  
26              
27 41     41 0 25486027 sub mvp_multivalue_args { qw(modules skip) }
28             sub mvp_aliases { {
29 41     41 0 5972 module => 'modules',
30             check_all => 'check_all_plugins',
31             } }
32              
33             has phase => (
34             is => 'ro',
35             isa => enum([qw(build release)]),
36             default => 'release',
37             );
38              
39             has modules => (
40             isa => 'ArrayRef[Str]',
41             traits => [ 'Array' ],
42             handles => { _raw_modules => 'elements' },
43             lazy => 1,
44             default => sub { [] },
45             );
46              
47             has check_authordeps => (
48             is => 'ro', isa => 'Bool',
49             default => 0,
50             );
51              
52             has check_all_plugins => (
53             is => 'ro', isa => 'Bool',
54             default => 0,
55             );
56              
57             has check_all_prereqs => (
58             is => 'ro', isa => 'Bool',
59             default => 0,
60             );
61              
62             has skip => (
63             isa => 'ArrayRef[Str]',
64             traits => [ 'Array' ],
65             handles => { skip => 'elements' },
66             lazy => 1,
67             default => sub { [] },
68             );
69              
70             has fatal => (
71             is => 'ro', isa => 'Bool',
72             default => 0,
73             );
74              
75             has index_base_url => (
76             is => 'ro', isa => 'Str',
77             );
78              
79             has run_under_travis => (
80             is => 'ro', isa => 'Bool',
81             default => 0,
82             );
83              
84             around dump_config => sub
85             {
86             my ($orig, $self) = @_;
87             my $config = $self->$orig;
88              
89             $config->{+__PACKAGE__} = {
90             (map +($_ => $self->$_ ? 1 : 0), qw(check_all_plugins check_all_prereqs run_under_travis)),
91             phase => $self->phase,
92             skip => [ sort $self->skip ],
93             modules => [ sort $self->_raw_modules ],
94             blessed($self) ne __PACKAGE__ ? ( version => $VERSION ) : (),
95             };
96              
97             return $config;
98             };
99              
100             sub before_build
101             {
102 26     26 0 180689 my $self = shift;
103              
104 26 100 66     386 if (not $ENV{PROMPTIFSTALE_REALLY_RUN_TESTS}
      100        
105             and $ENV{CONTINUOUS_INTEGRATION} and not $self->run_under_travis)
106             {
107 1         7 $self->log_debug('travis detected: skipping checks...');
108 1         416 return;
109             }
110              
111 25 100       1004 if ($self->phase eq 'build')
112             {
113 22         132 my @extra_modules = $self->_modules_extra;
114 22 100       796 my @modules = (
    100          
115             @extra_modules,
116             $self->check_authordeps ? $self->_authordeps : (),
117             $self->check_all_plugins ? $self->_modules_plugin : (),
118             );
119              
120 22 100       796 $self->log([ 'checking for stale %s...', join(', ',
    100          
    100          
121             @extra_modules ? 'modules' : (),
122             $self->check_authordeps ? 'authordeps' : (),
123             $self->check_all_plugins ? 'plugins' : ())
124             ]);
125 22 100       9818 $self->_check_modules(sort(uniq(@modules))) if @modules;
126             }
127             }
128              
129             sub after_build
130             {
131 19     19 0 754442 my $self = shift;
132              
133             return if not $ENV{PROMPTIFSTALE_REALLY_RUN_TESTS}
134 19 100 66     322 and $ENV{CONTINUOUS_INTEGRATION} and not $self->run_under_travis;
      100        
135              
136 18 100 100     710 if ($self->phase eq 'build' and $self->check_all_prereqs)
137             {
138 5 50       213 if (my @modules = $self->_modules_prereq) {
139 5         38 $self->log('checking for stale prerequisites...');
140 5         1438 $self->_check_modules(sort(uniq(@modules)));
141             }
142             }
143             }
144              
145             sub before_release
146             {
147 4     4 0 137997 my $self = shift;
148 4 100       196 if ($self->phase eq 'release')
149             {
150 3         51 my @extra_modules = $self->_modules_extra;
151 3 50       131 my @modules = (
    50          
    100          
152             @extra_modules,
153             $self->check_authordeps ? $self->_authordeps : (),
154             $self->check_all_plugins ? $self->_modules_plugin : (),
155             $self->check_all_prereqs ? $self->_modules_prereq : (),
156             );
157              
158 3 50       144 $self->log([ 'checking for stale %s...', join(', ',
    50          
    50          
    100          
159             @extra_modules ? 'modules' : (),
160             $self->check_authordeps ? 'authordeps' : (),
161             $self->check_all_plugins ? 'plugins' : (),
162             $self->check_all_prereqs ? 'prerequisites' : ())
163             ]);
164              
165 3 50       1995 $self->_check_modules(sort(uniq(@modules))) if @modules;
166             }
167             }
168              
169             # a package-scoped singleton variable that tracks the module names that have
170             # already been checked for, so other instances of this plugin do not duplicate
171             # the check.
172             my %already_checked;
173 33     33   107476 sub __clear_already_checked { %already_checked = () } # for testing
174              
175             # module name to absolute filename where the file can be found
176             my %module_to_filename;
177              
178             sub stale_modules
179             {
180 53     53 1 239 my ($self, @modules) = @_;
181              
182 53         61395 require Module::CoreList;
183 53         2191608 Module::CoreList->VERSION('5.20151213');
184              
185 53         3805 my $root = $self->zilla->root;
186 53         2196 my $root_volume = path($root)->volume;
187              
188 53         4407 my (@stale_modules, @errors);
189 53         571 foreach my $module (sort(uniq(@modules)))
190             {
191 198 100       583 $already_checked{$module}++ if $module eq 'perl';
192 198 100       1037 next if $already_checked{$module};
193              
194             # these core modules should be indexed, but aren't
195 187 100   740   2020 if (any { $module eq $_ } qw(Config DB Errno Pod::Functions))
  740         1748  
196             {
197 4         21 $self->log_debug([ 'skipping core module: %s', $module ]);
198 4         703 $already_checked{$module}++;
199 4         16 next;
200             }
201              
202 183         1205 my $path = Module::Metadata->find_module_by_name($module);
203 183 100       44702 if (not $path)
204             {
205 104         365 $already_checked{$module}++;
206 104         272 push @stale_modules, $module;
207 104         330 push @errors, $module . ' is not installed.';
208 104         251 next;
209             }
210              
211 79         269 $module_to_filename{$module} = $path;
212              
213             # ignore modules in the distribution currently being built
214 79 50       280 if (path($path)->volume eq $root_volume)
215             {
216 79         3923 my $relative_path = path($path)->relative($root);
217 79 100       33417 if ($relative_path !~ m/^\.\./)
218             {
219 2         18 $already_checked{$module}++;
220 2         9 $self->log_debug([ '%s provided locally (at %s); skipping version check',
221             $module, $relative_path->stringify ]);
222 2         380 next;
223             }
224             }
225              
226 77         882 my $indexed_version = $self->_indexed_version($module, !!(@modules > 5));
227 77         1630 my $local_version = Module::Metadata->new_from_file($module_to_filename{$module})->version;
228              
229             $self->log_debug([ 'comparing indexed vs. local version for %s: indexed=%s; local version=%s',
230 77   100 48   230124 $module, sub { ($indexed_version // 'undef') . '' }, sub { ($local_version // 'undef') . '' } ]);
  48   50     8229  
  48         707  
231              
232 77 100       10155 if (not defined $indexed_version)
233             {
234 38         109 $already_checked{$module}++;
235 38         77 push @stale_modules, $module;
236 38         126 push @errors, $module . ' is not indexed.';
237 38         133 next;
238             }
239              
240 39 100 66     555 if (defined $local_version
241             and $local_version < $indexed_version)
242             {
243 15         52 $already_checked{$module}++;
244              
245 15 100 100     63 if (Module::CoreList::is_core($module) and not $self->_is_duallifed($module))
246             {
247             $self->log_debug([ 'core module %s is indexed at version %s but you only have %s installed. You need to update your perl to get the latest version.',
248 4   50 3   530 $module, sub { ($indexed_version // 'undef') . '' }, sub { ($local_version // 'undef') . '' } ]);
  3   50     559  
  3         39  
249             }
250             else
251             {
252 11         12466 push @stale_modules, $module;
253 11         103 push @errors,
254             $module . ' is indexed at version ' . $indexed_version
255             . ' but you only have ' . $local_version . ' installed.';
256             }
257              
258 15         664 next;
259             }
260             }
261              
262 53         536 return [ sort @stale_modules ], [ sort @errors ];
263             }
264              
265             sub _check_modules
266             {
267 29     29   149 my ($self, @modules) = @_;
268              
269 29         146 my ($stale_modules, $errors) = $self->stale_modules(@modules);
270              
271 29 100       186 return if not @$errors;
272              
273 22 100       190 my $message = @$errors > 1
274             ? join("\n ", 'Issues found:', @$errors)
275             : $errors->[0];
276              
277             # just issue a warning if not being run interactively (e.g. |cpanm, travis)
278 22 100 66     990 if (($ENV{CONTINUOUS_INTEGRATION} and not $ENV{HARNESS_ACTIVE})
      66        
      66        
279             or not (-t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT))))
280             {
281 1         11 $self->log($message . "\n" . 'To remedy, do: cpanm ' . join(' ', @$stale_modules));
282 1         230 return;
283             }
284              
285 21         73 my $continue;
286 21 100       962 if ($self->fatal)
287             {
288 3         16 $self->log($message);
289             }
290             else
291             {
292 18 100       571 $continue = $self->zilla->chrome->prompt_yn(
293             $message . (@$errors > 1 ? "\n" : ' ') . 'Continue anyway?',
294             { default => 0 },
295             );
296             }
297              
298 21 100       22771 $self->log_fatal('Aborting ' . $self->phase . "\n"
299             . 'To remedy, do: cpanm ' . join(' ', @$stale_modules)) if not $continue;
300             }
301              
302             has _authordeps => (
303             isa => 'ArrayRef[Str]',
304             traits => ['Array'],
305             handles => { _authordeps => 'elements' },
306             lazy => 1,
307             default => sub {
308             my $self = shift;
309             require Dist::Zilla::Util::AuthorDeps;
310             Dist::Zilla::Util::AuthorDeps->VERSION(5.021);
311             my @skip = $self->skip;
312             [
313             grep { my $module = $_; none { $module eq $_ } @skip }
314             uniq(
315             map +(%$_)[0],
316             @{ Dist::Zilla::Util::AuthorDeps::extract_author_deps($self->zilla->root) }
317             )
318             ];
319             },
320             );
321              
322             has _modules_plugin => (
323             isa => 'ArrayRef[Str]',
324             traits => ['Array'],
325             handles => { _modules_plugin => 'elements' },
326             lazy => 1,
327             default => sub {
328             my $self = shift;
329             my @skip = $self->skip;
330             [
331             # we look on disk since it is too early to check $zilla->files
332             # TODO: do this properly by moving the caller to after_build
333             grep { (my $filename = $_) =~ s{::}{/}g; !-f "${filename}.pm" }
334             grep { my $module = $_; none { $module eq $_ } @skip }
335             uniq
336             map find_meta($_)->name,
337             eval { Dist::Zilla->VERSION('7.000') } ? $self->zilla->plugins : @{ $self->zilla->plugins }
338             ];
339             },
340             );
341              
342             has _modules_prereq => (
343             isa => 'ArrayRef[Str]',
344             traits => ['Array'],
345             handles => { _modules_prereq => 'elements' },
346             lazy => 1,
347             default => sub {
348             my $self = shift;
349             my $prereqs = $self->zilla->prereqs->as_string_hash;
350             my @skip = $self->skip;
351             [
352             grep { my $module = $_; none { $module eq $_ } @skip }
353             map keys %$_,
354             grep defined,
355             map @{$_}{qw(requires recommends suggests)},
356             grep defined,
357             values %$prereqs
358             ];
359             },
360             );
361              
362             sub _modules_extra
363             {
364 54     54   144 my $self = shift;
365 54         2178 my @skip = $self->skip;
366 54     12   2348 grep { my $module = $_; none { $module eq $_ } @skip } $self->_raw_modules;
  121         240  
  121         1649  
  12         57  
367             }
368              
369             # this ought to be in Module::CoreList -- TODO :)
370             sub _is_duallifed
371             {
372 6     6   25551 my ($self, $module) = @_;
373              
374 6 50       26 return if not Module::CoreList::is_core($module);
375              
376             # Module::CoreList doesn't tell us this information at all right now - for
377             # blead-upstream dual-lifed modules, and non-dual-lifed modules, it
378             # returns all the same data points. :( Right now all we can do is query
379             # the index and see what distribution it belongs to -- luckily, it still lists the
380             # cpan distribution for dual-lifed modules that are more recent in core than on
381             # CPAN (e.g. Carp in June 2014 is 1.34 in 5.20.0 but 1.3301 on cpan).
382              
383 6         24331 my $url = 'http://cpanmetadb.plackperl.org/v1.0/package/' . $module;
384 6         43 $self->log_debug([ 'fetching %s', $url ]);
385 6         1711 my $res = HTTP::Tiny->new->get($url);
386 6 50       4532 $self->log('could not query the index?'), return undef if not $res->{success};
387              
388 6         17 my $data = $res->{content};
389              
390 6         46 require HTTP::Headers;
391 6 50       15 if (my $charset = HTTP::Headers->new(%{ $res->{headers} })->content_type_charset)
  6         50  
392             {
393 0         0 $data = Encode::decode($charset, $data, Encode::FB_CROAK);
394             }
395 6         776 $self->log_debug([ 'got response: %s', $data ]);
396              
397 6         1595 my $payload = YAML::Tiny->read_string($data);
398              
399 6 50       1229 $self->log('invalid payload returned?'), return undef unless $payload;
400 6 50       29 $self->log_debug([ '%s not indexed', $module ]), return undef if not defined $payload->[0]{distfile};
401 6         59 return CPAN::DistnameInfo->new($payload->[0]{distfile})->dist ne 'perl';
402             }
403              
404             my $packages;
405             sub _indexed_version
406             {
407 42     42   120 my ($self, $module, $combined) = @_;
408              
409             # we download 02packages if we have several modules to query at once, or
410             # if we were given a different URL to use -- otherwise, we perform an API
411             # hit for just this one module's data
412 42 100 66     268 return $combined || $packages || $self->_has_index_base_url
413             ? $self->_indexed_version_via_02packages($module)
414             : $self->_indexed_version_via_query($module);
415             }
416              
417             # I bet this is available somewhere as a module?
418             sub _indexed_version_via_query
419             {
420 8     8   31 my ($self, $module) = @_;
421              
422 8 50       28 die 'should not be here - get 02packages instead' if $self->_has_index_base_url;
423 8 50       29 die 'no module?' if not $module;
424              
425 8         27 my $url = 'http://cpanmetadb.plackperl.org/v1.0/package/' . $module;
426 8         67 $self->log_debug([ 'fetching %s', $url ]);
427 8         2186 my $res = HTTP::Tiny->new->get($url);
428 8 50       7265 $self->log('could not query the index?'), return undef if not $res->{success};
429              
430 8         53 my $data = $res->{content};
431              
432 8         2132 require HTTP::Headers;
433 8 50       12195 if (my $charset = HTTP::Headers->new(%{ $res->{headers} })->content_type_charset)
  8         74  
434             {
435 0         0 $data = Encode::decode($charset, $data, Encode::FB_CROAK);
436             }
437 8         5689 $self->log_debug([ 'got response: %s', $data ]);
438              
439 8         1949 my $payload = YAML::Tiny->read_string($data);
440              
441 8 50       2308 $self->log('invalid payload returned?'), return undef unless $payload;
442 8 50       35 $self->log_debug([ '%s not indexed', $module ]), return undef if not defined $payload->[0]{version};
443 8         135 version->parse($payload->[0]{version});
444             }
445              
446             # TODO: it would be AWESOME to provide this to multiple plugins via a role
447             # even better would be to save the file somewhere semi-permanent and
448             # keep it refreshed with a Last-Modified header - or share cpanm's copy?
449             sub _get_packages
450             {
451 34     34   173 my $self = shift;
452 34 100       91 return $packages if $packages;
453              
454 2         12 my $tempdir = Path::Tiny->tempdir(CLEANUP => 1);
455 2         1208 my $filename = '02packages.details.txt.gz';
456 2         10 my $path = $tempdir->child($filename);
457              
458             # We don't set this via an attribute default because we want to
459             # distinguish the case where this was not set at all.
460 2   50     129 my $base = $self->index_base_url || $ENV{CPAN_INDEX_BASE_URL} || 'http://www.cpan.org';
461              
462 2         8 my $url = $base . '/modules/' . $filename;
463 2         19 $self->log_debug([ 'fetching %s', $url ]);
464 2         421 my $response = HTTP::Tiny->new->mirror($url, $path);
465 2 50       243 $self->log('could not fetch the index - network down?'), return undef if not $response->{success};
466              
467 2         14 require Parse::CPAN::Packages::Fast;
468 2         11 $packages = Parse::CPAN::Packages::Fast->new($path->stringify);
469             }
470              
471             sub _has_index_base_url {
472 16     16   33 my $self = shift;
473 16   33     712 return $self->index_base_url || $ENV{CPAN_INDEX_BASE_URL};
474             }
475              
476             sub _indexed_version_via_02packages
477             {
478 0     0     my ($self, $module) = @_;
479              
480 0 0         die 'no module?' if not $module;
481 0           my $packages = $self->_get_packages;
482 0 0         return undef if not $packages;
483 0           my $package = $packages->package($module);
484 0 0         return undef if not $package;
485 0           version->parse($package->version);
486             }
487              
488             __PACKAGE__->meta->make_immutable;
489              
490             __END__
491              
492             =pod
493              
494             =encoding UTF-8
495              
496             =head1 NAME
497              
498             Dist::Zilla::Plugin::PromptIfStale - Check at build/release time if modules are out of date
499              
500             =head1 VERSION
501              
502             version 0.057
503              
504             =head1 SYNOPSIS
505              
506             In your F<dist.ini>:
507              
508             [PromptIfStale]
509             phase = build
510             module = Dist::Zilla
511             module = Dist::Zilla::PluginBundle::Author::ME
512              
513             or:
514              
515             [PromptIfStale]
516             check_all_plugins = 1
517              
518             =head1 DESCRIPTION
519              
520             C<[PromptIfStale]> is a C<BeforeBuild> or C<BeforeRelease> plugin that compares the
521             locally-installed version of a module(s) with the latest indexed version,
522             prompting to abort the build process if a discrepancy is found.
523              
524             Note that there is no effect on the built distribution -- all actions are taken at
525             build time.
526              
527             =head1 CONFIGURATION OPTIONS
528              
529             =head2 C<phase>
530              
531             Indicates whether the checks are performed at I<build> or I<release> time
532             (defaults to I<release>).
533              
534             (Remember that you can use different settings for different phases by employing
535             this plugin twice, with different names.)
536              
537             =head2 C<module>
538              
539             The name of a module to check for. Can be provided more than once.
540              
541             =head2 C<check_authordeps>
542              
543             =for stopwords authordeps
544              
545             A boolean, defaulting to false, indicating that all authordeps in F<dist.ini>
546             (like what is done by C<< dzil authordeps >>) should be checked.
547              
548             As long as this option is not explicitly set to false, a check is always made
549             for authordeps being installed (but the indexed version is not checked). This
550             serves as a fast way to guard against a build blowing up later through the
551             inadvertent lack of fulfillment of an explicit C<< ; authordep >> declaration.
552              
553             =head2 C<check_all_plugins>
554              
555             A boolean, defaulting to false, indicating that all plugins being used to
556             build this distribution should be checked.
557              
558             =head2 C<check_all_prereqs>
559              
560             A boolean, defaulting to false, indicating that all prerequisites in the
561             distribution metadata should be checked. The modules are a merged list taken
562             from all phases (C<configure>, C<build>, C<runtime>, C<test> and C<develop>) ,
563             and the C<requires>, C<recommends> and C<suggests> types.
564              
565             =head2 C<skip>
566              
567             The name of a module to exempt from checking. Can be provided more than once.
568              
569             =head2 C<fatal>
570              
571             A boolean, defaulting to false, indicating that missing prereqs will result in
572             an immediate abort of the build/release process, without prompting.
573              
574             =head2 C<index_base_url>
575              
576             =for stopwords darkpan
577              
578             When provided, uses this base URL to fetch F<02packages.details.txt.gz>
579             instead of the default C<http://www.cpan.org>. Use this when your
580             distribution uses prerequisites found only in your darkpan-like server.
581              
582             You can also set this temporary from the command line by setting the
583             C<CPAN_INDEX_BASE_URL> environment variable.
584              
585             =head2 C<run_under_travis>
586              
587             It is possible to detect when a build is being run via L<Travis Continuous Integration|https://travis-ci.org/>.
588             Since version 0.035, Travis builds act like other non-interactive builds, where missing modules result in a warning
589             instead of a prompt. As of version 0.050, stale checks are only performed for the build phase on Travis builds when
590             C<run_under_travis> is set to a true value.
591              
592             The default value is false.
593              
594             =for Pod::Coverage mvp_multivalue_args mvp_aliases before_build after_build before_release
595              
596             =head1 METHODS
597              
598             =head2 stale_modules
599              
600             Given a list of modules to check, returns
601              
602             =over 4
603              
604             =item *
605              
606             a list reference of modules that are stale (not installed or the version is not at least the latest indexed version
607              
608             =item *
609              
610             a list reference of error messages describing the issues found
611              
612             =back
613              
614             =head1 SEE ALSO
615              
616             =over 4
617              
618             =item *
619              
620             the L<[EnsureNotStale]|Dist::Zilla::Plugin::EnsureNotStale> plugin in this distribution
621              
622             =item *
623              
624             the L<dzil stale|Dist::Zilla::App::Command::stale> command in this distribution
625              
626             =item *
627              
628             L<Dist::Zilla::Plugin::Prereqs::MatchInstalled>, L<Dist::Zilla::Plugin::Prereqs::MatchInstalled::All>
629              
630             =back
631              
632             =head1 SUPPORT
633              
634             Bugs may be submitted through L<the RT bug tracker|https://rt.cpan.org/Public/Dist/Display.html?Name=Dist-Zilla-Plugin-PromptIfStale>
635             (or L<bug-Dist-Zilla-Plugin-PromptIfStale@rt.cpan.org|mailto:bug-Dist-Zilla-Plugin-PromptIfStale@rt.cpan.org>).
636              
637             There is also a mailing list available for users of this distribution, at
638             L<http://dzil.org/#mailing-list>.
639              
640             There is also an irc channel available for users of this distribution, at
641             L<C<#distzilla> on C<irc.perl.org>|irc://irc.perl.org/#distzilla>.
642              
643             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.freenode.org>.
644              
645             =head1 AUTHOR
646              
647             Karen Etheridge <ether@cpan.org>
648              
649             =head1 CONTRIBUTORS
650              
651             =for stopwords David Golden Dave Rolsky Olivier Mengué
652              
653             =over 4
654              
655             =item *
656              
657             David Golden <dagolden@cpan.org>
658              
659             =item *
660              
661             Dave Rolsky <autarch@urth.org>
662              
663             =item *
664              
665             Olivier Mengué <dolmen@cpan.org>
666              
667             =back
668              
669             =head1 COPYRIGHT AND LICENCE
670              
671             This software is copyright (c) 2013 by Karen Etheridge.
672              
673             This is free software; you can redistribute it and/or modify it under
674             the same terms as the Perl 5 programming language system itself.
675              
676             =cut