File Coverage

lib/Dist/Zilla/PluginBundle/Author/GSG.pm
Criterion Covered Total %
statement 71 77 92.2
branch 14 16 87.5
condition 13 16 81.2
subroutine 14 16 87.5
pod 1 3 33.3
total 113 128 88.2


line stmt bran cond sub pod time code
1             package Dist::Zilla::PluginBundle::Author::GSG;
2              
3             # ABSTRACT: Grant Street Group CPAN dists
4 1     1   229101 use version;
  1         10  
  1         11  
5             our $VERSION = 'v0.5.0'; # VERSION
6              
7 1     1   98 use Carp;
  1         2  
  1         64  
8 1     1   7 use Git::Wrapper;
  1         1  
  1         21  
9              
10 1     1   677 use Moose;
  1         456246  
  1         6  
11             with qw(
12             Dist::Zilla::Role::PluginBundle::Easy
13             );
14 1     1   8269 use namespace::autoclean;
  1         3  
  1         12  
15              
16             #pod =for Pod::Coverage configure mvp_multivalue_args
17             #pod
18             #pod =cut
19              
20 13     13 0 5032368 sub mvp_multivalue_args { qw(
21             exclude_filename
22             exclude_match
23             test_compile_skip
24             test_compile_file
25             test_compile_module_finder
26             test_compile_script_finder
27             test_compile_switch
28             dont_munge
29             ) }
30              
31             sub configure {
32 16     16 0 317 my ($self) = @_;
33              
34             my $meta_provides
35 16   100     704 = 'MetaProvides::' . ( $self->payload->{meta_provides} || 'Package' );
36              
37 16         629 $self->add_bundle( 'Filter' => {
38             -bundle => '@Basic',
39             -remove => [ qw(
40             UploadToCPAN
41             GatherDir
42             ) ]
43             } );
44              
45             # We need to reconfigure the MakeMaker Plugin to require
46             # a new enough version to support "version ranges".
47             # https://github.com/Perl-Toolchain-Gang/ExtUtils-MakeMaker/issues/215
48             my ($mm)
49 192         475 = grep { $_->[1] eq 'Dist::Zilla::Plugin::MakeMaker' }
50 16         99883 @{ $self->plugins };
  16         629  
51              
52 16         142 $mm->[2]->{eumm_version} = '7.1101';
53              
54 16         520 my $name = $self->name;
55              
56             $self->add_plugins(
57             'Author::GSG',
58              
59             [ 'FileFinder::Filter' => 'MungeableFiles' => {
60             finder => [ ':InstallModules', ':PerlExecFiles' ],
61 16         122 %{ $self->config_slice({ dont_munge => 'skip' }) }
62             }],
63              
64             'MetaJSON',
65             [ 'OurPkgVersion' => {
66             finder => [ "$name/MungeableFiles" ],
67             semantic_version => 1,
68             } ],
69             'Prereqs::FromCPANfile',
70             $meta_provides,
71              
72             [ 'StaticInstall' => $self->config_slice( {
73             static_install_mode => 'mode',
74             static_install_dry_run => 'dry_run',
75             } ) ],
76              
77             # StaticInstall wants scripts in a script/ ExecDir
78             [ 'ExecDir' => { dir => 'script' } ],
79              
80             [ 'PodWeaver' => {
81             finder => [ "$name/MungeableFiles" ],
82             replacer => 'replace_with_comment',
83             post_code_replacer => 'replace_with_nothing',
84             config_plugin => [ '@Default', 'Contributors' ]
85             } ],
86              
87             'ReadmeAnyFromPod',
88             [ 'ChangelogFromGit::CPAN::Changes' => {
89             file_name => 'CHANGES',
90             # Support both old 0.90 versioning and new v1.2.3 semantic versioning formats
91             tag_regexp => '\b(v?\d+\.\d+(?:\.\d+)*)\b',
92             copy_to_root => 0,
93             } ],
94              
95             [ 'Author::GSG::Git::NextVersion' => {
96             first_version => 'v0.0.1',
97             version_regexp => '\b(v\d+\.\d+\.\d+)\b',
98             } ],
99              
100             'Git::Commit',
101             'Git::Tag',
102             'Git::Push',
103              
104             [ 'Git::GatherDir' => $self->config_slice( qw<
105             exclude_filename
106             exclude_match
107             include_dotfiles
108             > ) ],
109              
110             'GitHub::Meta',
111             'Author::GSG::GitHub::UploadRelease',
112              
113             [ 'Test::Compile' => $self->config_slice( {
114 16         256 map {; "test_compile_$_" => $_ } qw<
  192         5008  
115             filename
116             phase
117             skip
118             file
119             fake_home
120             needs_display
121             fail_on_warning
122             bail_out_on_fail
123             module_finder
124             script_finder
125             xt_mode
126             switch
127             >
128             } ) ],
129              
130             'Test::ReportPrereqs',
131             'HasVersionTests',
132             'PodSyntaxTests',
133             'PodCoverageTests',
134             );
135              
136             my ($gather_dir)
137 560         1182 = grep { $_->[1] eq 'Dist::Zilla::Plugin::Git::GatherDir' }
138 16         13189 @{ $self->plugins };
  16         616  
139              
140 16         75 push @{ $gather_dir->[2]->{exclude_filename} },
  16         114  
141             qw< README.md LICENSE.txt >;
142              
143             # By default we want to set the github remote,
144             # but "subclasses" may not want that, so make them
145             # calculate the github_remote themselves.
146             $self->payload->{find_github_remote} //=
147 16   100     625 $self->name eq '@Author::GSG';
148              
149             # Look for the GitHub remote, or fail, if we are supposed to.
150 16 100       1443 if ( $self->payload->{find_github_remote} ) {
151 13   66     536 $self->payload->{github_remote} //= $self->_find_github_remote
      100        
152             // croak "Unable to find git remote for GitHub";
153             }
154              
155             $self->_set_github_remote( $self->payload->{github_remote} )
156             if defined $self->payload->{github_remote}
157 12 100 66     913 and length $self->payload->{github_remote};
158             }
159              
160              
161             sub _set_github_remote {
162 9     9   1206 my ( $self, $remote ) = @_;
163              
164 9         59 my @git;
165             my @github;
166              
167 9         33 foreach my $plugin ( @{ $self->plugins } ) {
  9         348  
168 315 100       1261 if ( $plugin->[1] =~ /GitHub/ ) {
    100          
169 18         112 push @github, $plugin;
170             }
171             elsif ( $plugin->[1] =~ /Git/ ) {
172 54         114 push @git, $plugin;
173             }
174             }
175              
176             # All our git/github plugins configure this way, so good enough?
177 9         248 $_->[2]->{push_to} = [$remote] for @git;
178 9         127 $_->[2]->{remote} = $remote for @github;
179              
180 9         97 return $remote;
181             }
182              
183             sub _find_github_remote {
184 8     8   212 my ($self) = @_;
185              
186             # If it's a git issue finding the remote,
187             # the user can figure it out.
188 8         21 my @remotes = do { local $@; eval { local $SIG{__DIE__};
  8         18  
  8         26  
  8         105  
189 8         152 Git::Wrapper->new('.')->remote('-v') } };
190              
191 8         143230 my $remote;
192              
193 8         60 for (@remotes) {
194 16         304 my ( $name, $url, $direction )
195 1     1   1002 = /^ (\P{PosixCntrl}+) \s+ (.*) \s+ \( ([^)]+) \) $/x;
  1         3  
  1         16  
196              
197 16 100 50     149 next unless ( $direction || '' ) eq 'push';
198              
199 8 100       264 if ( $url
200             =~ m{(?: :// | \@ ) (?: [\w\-\.]+\. )? github\.com [/:] }ix )
201             {
202 6 100       827 croak "Multiple git remotes found for GitHub" if defined $remote;
203 5         35 $remote = $name;
204             }
205             }
206              
207 7         2496 return $remote;
208             }
209              
210              
211             __PACKAGE__->meta->make_immutable;
212              
213             package # hide from the CPAN
214             Dist::Zilla::Plugin::Author::GSG::GitHub::UploadRelease;
215 1     1   22905 use Moose;
  1         3  
  1         8  
216 1     1   6681 BEGIN { extends 'Dist::Zilla::Plugin::GitHub::UploadRelease' }
217             with qw(
218             Dist::Zilla::Role::Releaser
219             );
220              
221 0     0 1   sub release {1} # do nothing, just let the GitHub Uploader do it for us
222              
223             # TODO: on release, regen README.md in src dir
224              
225             around 'after_release' => sub {
226             my ($orig, $self, @args) = @_;
227              
228             my $git_tag_plugin = $self->zilla->plugin_named('@Author::GSG/Git::Tag')
229             or $self->log_fatal('Plugin @Author::GSG/Git::Tag not found!');
230              
231             # GitHub::UploadRelease looks for the Git::Tag Plugin with this name
232             local $git_tag_plugin->{plugin_name} = 'Git::Tag';
233              
234             return $self->$orig(@args);
235             };
236              
237             sub _get_credentials {
238 0     0     my ($self, $login_only) = @_;
239              
240 0           my $creds = $self->_credentials;
241             # return $creds->{login} if $login_only;
242              
243 0           my $otp;
244 0 0         $otp = $self->zilla->chrome->prompt_str(
245             "GitHub two-factor authentication code for '$creds->{login}'",
246             { noecho => 1 },
247             ) if $self->prompt_2fa;
248              
249 0           return ( $creds->{login}, $creds->{pass}, $otp );
250             }
251              
252             __PACKAGE__->meta->make_immutable;
253              
254             package # hide from the CPAN
255             Dist::Zilla::Plugin::Author::GSG::Git::NextVersion;
256 1     1   508569 use Moose;
  1         3  
  1         6  
257 1     1   7059 BEGIN { extends 'Dist::Zilla::Plugin::Git::NextVersion' }
258              
259             before 'provide_version' => sub {
260             if ( my $v = $ENV{V} ) {
261             $v =~ s/^v//;
262             my @v = split /\./, $v;
263              
264             Carp::croak "Invalid version '$ENV{V}' in \$ENV{V}"
265             if @v > 3 or grep /\D/, @v;
266              
267             # perl v5.22+ complain about too many arguments to printf
268             $ENV{V} = sprintf "v%d.%d.%d", (@v, 0, 0, 0)[0..2];
269             }
270             };
271              
272             __PACKAGE__->meta->make_immutable;
273              
274             1;
275              
276             __END__
277              
278             =pod
279              
280             =encoding UTF-8
281              
282             =head1 NAME
283              
284             Dist::Zilla::PluginBundle::Author::GSG - Grant Street Group CPAN dists
285              
286             =head1 VERSION
287              
288             version v0.5.0
289              
290             =head1 SYNOPSIS
291              
292             Your C<dist.ini> can be as short as this:
293              
294             name = Foo-Bar-GSG
295             [@Author::GSG]
296              
297             Which is equivalent to all of this:
298              
299             Some of which comes from L<Dist::Zilla::Plugin::Author::GSG>.
300              
301             name = Foo-Bar-GSG
302             author = Grant Street Group <developers@grantstreet.com>
303             license = Artistic_2_0
304             copyright_holder = Grant Street Group
305             copyright_year = # detected from git
306              
307             [@Filter]
308             -bundle = @Basic
309             -remove = UploadToCPAN
310             -remove = GatherDir
311              
312             ; The MakeMaker Plugin gets an additional setting
313             ; in order to support "version ranges".
314             eumm_version = 7.1101
315              
316             ; We try to guess which remote to use to talk to GitHub
317             ; but you can hardcode a value if necessary
318             github_remote = # detected from git if find_github_remote is set
319              
320             ; Enabled by default if the PluginBundle name is Author::GSG
321             ; This means Filters do not automatically get it set
322             find_github_remote = 1
323              
324             ; The defaults for author and license come from
325             [Author::GSG]
326              
327             [ FileFinder::Filter / MungeableFiles ]
328             finder => :InstallModules
329             finder => :PerlExecFiles
330             ; dont_munge = (?^:bin) # can be used multiple times. passed in as "skip"
331              
332             [MetaJSON]
333             [OurPkgVersion]
334             finder = :MungableFiles
335             [Prereqs::FromCPANfile]
336             [$meta_provides] # defaults to MetaProvides::Package
337              
338             [StaticInstall]
339             ; mode from static_install_mode
340             ; dry_run from static_install_dry_run
341              
342             [ExecDir]
343             dir = script # in addition to bin/ for StaticInstall compatibility
344              
345             [PodWeaver]
346             finder = :MungeableFiles
347             replacer = replace_with_comment
348             post_code_replacer = replace_with_nothing
349             config_plugin = [ @Default, Contributors ]
350              
351             [ReadmeAnyFromPod]
352             [ChangelogFromGit::CPAN::Changes]
353             file_name = CHANGES
354             ; Support both old 0.90 versioning and new v1.2.3 semantic versioning formats
355             tag_regexp = \b(v?\d+\.\d+(?:\.\d+)*)\b
356             copy_to_root = 0
357              
358             [Git::NextVersion] # plus magic to sanitize versions from the environment
359             first_version = v0.0.1
360             version_regexp = \b(v\d+\.\d+\.\d+)(?:\.\d+)*\b
361              
362             [Git::Commit]
363             [Git::Tag]
364             [Git::Push]
365              
366             [Git::GatherDir]
367             ; include_dotfiles
368             ; exclude_filename
369             ; exclude_match
370             exclude_filename = README.md
371             exclude_filename = LICENSE.txt
372              
373             [GitHub::Meta]
374             [GitHub::UploadRelease] # plus magic to work without releasing elsewhere
375              
376             [Test::Compile]
377             ; test_compile_filename
378             ; test_compile_phase
379             ; test_compile_skip
380             ; test_compile_file
381             ; test_compile_fake_home
382             ; test_compile_needs_display
383             ; test_compile_fail_on_warning
384             ; test_compile_bail_out_on_fail
385             ; test_compile_module_finder
386             ; test_compile_script_finder
387             ; test_compile_xt_mode
388             ; test_compile_switch
389              
390             [Test::ReportPrereqs]
391             [HasVersionTests]
392             [PodSyntaxTests]
393             [PodCoverageTests]
394              
395             =head1 DESCRIPTION
396              
397             This PluginBundle is here to make it easy for folks at GSG to release
398             public distributions of modules as well as trying to make it easy for
399             other folks to contribute.
400              
401             The C<share_dir> for this module includes GSG standard files to include
402             with open source modules. Things like a standard Makefile
403             and a contributing guide.
404             See the L</update> Makefile target for details.
405              
406             The expected workflow for a module using this code is that after following
407             the initial setup decribed below, you would manage changes via standard
408             GitHub flow pull requests and issues.
409             When ready for a release, you would first C<make update> to update
410             any included documents, commit those,
411             and then run C<carton exec dzil release>.
412             You can set a specific release version with the C<V> environment variable,
413             as described in the
414             L<Git::NextVersion Plugin|Dist::Zilla::Plugin::Git::NextVersion> documentation.
415              
416             The version regexps for both the Changelog and NextVersion
417             should be open enough to pick up the older style tags we used
418             as well as incrementing a more strict C<semver>.
419              
420             =for Pod::Coverage configure mvp_multivalue_args
421              
422             =head1 ATTRIBUTES / PARAMETERS
423              
424             =over
425              
426             =item github_remote / find_github_remote
427              
428             Looks in the C<git remote> list for a C<push> remote that matches
429             C<github.com> (case insensitively) and if we find one,
430             we pass it to the Git and GitHub Plugins we use.
431              
432             If no remotes or multiple remotes are found, throws an exception
433             indicating that you need to add the GitHub remote as described in
434             L</Cutting a release>.
435              
436             Trying to find a remote, and failing if it isn't found,
437             is only enabled if you set C<find_github_remote> to a truthy value.
438             However, C<find_github_remote> defaults to truthy if the section
439             name for the PluginBundle is the default, C<@Author::GSG>.
440              
441             You can disable this, and fall back to each Plugin's default,
442             by setting C<github_remote> to an empty string.
443              
444             =item meta_provides
445              
446             [@Author::GSG]
447             meta_provides = Class
448              
449             The L<MetaProvides|Dist::Zilla::Plugin::MetaProvides> subclass to use.
450              
451             Defaults to C<Package|Dist::Zilla::Plugin::MetaProvides::Package>.
452              
453             If you choose something other than the default,
454             you will need to add an "on develop" dependency to your C<cpanfile>.
455              
456             =item static_install_mode
457              
458             Passed to L<Dist::Zilla::Plugin::StaticInstall> as C<mode>.
459              
460             =item static_install_dry_run
461              
462             Passed to L<Dist::Zilla::Plugin::StaticInstall> as C<dry_run>.
463              
464             =item include_dotfiles
465              
466             Passed to L<Dist::Zilla::Plugin::Git::GatherDir/include_dotfiles>.
467              
468             =item exclude_filename
469              
470             Passed to L<Dist::Zilla::Plugin::Git::GatherDir/exclude_filename>.
471              
472             Automatically appends C<README.md> and C<LICENSE.txt> to the list.
473              
474             =item exclude_match
475              
476             Passed to L<Dist::Zilla::Plugin::Git::GatherDir/exclude_match>.
477              
478             =item test_compile_*
479              
480             [@Author::GSG]
481             test_compile_skip = ^My::NonCompiling::Module$
482             test_compile_xt_mode = 1
483              
484             All options for L<Dist::Zilla::Plugin::Test::Compile> should be supported
485             with the C<test_compile_> prefix.
486              
487             =item dont_munge
488              
489             [@Author::GSG]
490             dont_munge = (?^:one-off)
491             dont_munge = (?^:docs/.*.txt)
492              
493             Passed to L<Dist::Zilla::Plugin::FileFinder::Filter> as c<skip> for the
494             C<MungableFiles> plugin.
495              
496             This plugin gets passed to L<Dist::Zilla::Plugin::OurPkgVersion> and
497             L<Dist::Zilla::Plugin::PodWeaver> as C<finder> to filter matches.
498              
499             =back
500              
501             =head1 Setting up a new dist
502              
503             =head2 Create your dist.ini
504              
505             As above, you need the C<name> and C<[@Author::GSG]> bundle,
506             plus any other changes you need.
507              
508             =head2 Add Dist::Zilla::PluginBundle::Author::GSG to your cpanfile
509              
510             on 'develop' => sub {
511             requires 'Dist::Zilla::PluginBundle::Author::GSG';
512             };
513              
514             Doing this in the C<develop> phase will cause the default Makefile
515             not to install it, which means folks contributing to a module
516             won't need to install all of the Dist::Zilla dependencies just to
517             submit some patches, but will be able to run most tests.
518              
519             =head2 Create a Makefile
520              
521             It is recommended to keep a copy of the Makefile from this PluginBundle
522             in your app and update it as necessary, which the target in the included
523             Makefile will do automatically.
524              
525             An initial Makefile you could use to copy one out of this PluginBundle
526             might look like this:
527              
528             SHARE_DIR := $(shell \
529             carton exec perl -Ilib -MFile::ShareDir=dist_dir -e \
530             'print eval { dist_dir("Dist-Zilla-PluginBundle-Author-GSG") } || "share"' )
531              
532             include $(SHARE_DIR)/Makefile.inc
533              
534             # Copy the SHARE_DIR Makefile over this one:
535             # Making it .PHONY will force it to copy even if this one is newer.
536             .PHONY: Makefile
537             Makefile: $(SHARE_DIR)/Makefile.inc
538             cp $< $@
539              
540             Using this example Makefile does require you run C<carton install> after
541             adding the C<on 'develop'> dependency to your cpanfile as described above.
542              
543             If you want to override the Makefile included with this Plugin
544             but still want to use some of the targets in it,
545             you can rename it to something such as C<Makefile.inc>
546             and include it from your C<Makefile>.
547              
548             Also see the section on L<Makefile.PL> and XS modules.
549              
550             The Makefile that comes in this PluginBundle's C<share_dir> has a many
551             helpers to make development on a module supported by it easier.
552              
553             Some of the targets that are included in the Makefile are:
554              
555             =over
556              
557             =item test
558              
559             Makes your your C<local> C<cpanfile.snapshot> is up-to-date and
560             if not, will run L<Carton> before running C<prove -lfr t>.
561              
562             =item testcoverage
563              
564             This target runs your tests under the L<Devel::Cover> C<cover> utility.
565             However, C<Devel::Cover> is not normally a dependency,
566             so you will need to add it to the cpanfile temporarily for this target to work.
567              
568             =item Makefile
569              
570             Copies the C<Makefile.inc> included in this PluginBundle's C<share_dir>
571             into your distribution.
572              
573             Actually the C<$(lastword $(MAKEFILE_LIST))>,
574             so if you put the Makefile somewhere else,
575             for example C<Makefile.inc> or C<ci/Makefile>,
576             that will be the target.
577              
578             This should happen automatically through the magic of C<make>.
579              
580             =item update
581              
582             Generates README.md and copies some additional files from this
583             PluginBundle's C<share_dir> into the repo so that the shared
584             documents provided here will be kept up-to-date.
585              
586             =over
587              
588             =item README.md
589              
590             This is generated from the post C<Pod::Weaver> documentation of the
591             main module in the dist.
592             Requires installing the C<develop> cpanfile dependencies to work.
593              
594             =item $(CONTRIB)
595              
596             The files in this variable are copied from this PluginBundle's
597              
598             Currently includes C<CONTRIBUTING.md>.
599              
600             =back
601              
602             =item $(CPANFILE_SNAPSHOT)
603              
604             Attempts to locate the correct C<cpanfile.snapshot> and
605             automatically runs C<carton install $(CARTON_INSTALL_FLAGS)> if
606             it is out of date.
607              
608             The C<CARTON_INSTALL_FLAGS> are by default C<--without develop>
609             in order to avoid unnecessarily installing the heavy C<Dist::Zilla>
610             dependency chain.
611              
612             =back
613              
614             =head2 Makefile.PL
615              
616             There is special support for a C<Makefile.PL> in the root of the repository.
617             If using it, you will need to rename the C<Makefile> included with this
618             PluginBundle to something else, for example C<ci/Makefile> or C<Makefile.dzil>.
619              
620             This is normally used for modules that need a build phase before they
621             will pass tests, such as modules that use XS.
622              
623             Generating the initial C<Makefile.PL> can be done with:
624              
625             cp Makefile Makefile.dzil
626             touch Makefile.PL dist.ini
627             make Makefile.PL
628             echo Makefile >> .gitignore
629             git rm -f Makefile && git commit -am"Switch to Makefile.PL"
630              
631             Which should copy the Makefile.PL from the dzil workspace into your
632             repostitory.
633              
634             Because this PluginBundle uses L<Dist::Zilla::Plugin::OurPkgVersion>,
635             actually testing XS modules is a bit troublesome because they don't
636             have a C<$VERSION> available.
637             The above command will generate the C<Makefile.PL> with an embedded
638             version of C<v0.0.1> which can be used in your tests to work around this.
639             First you need to add C<use vars '$VERSION';> to the C<.pm> files that
640             C<bootstrap> the XS module, and then in your tests that load that module,
641             before you C<use> the module to be tested,
642             you can set a default version that will match the the one in the Makefile.PL.
643             This default will be overridden when My::Module is loaded if it has a
644             C<VERSION> of its own.
645              
646             BEGIN { $My::Module::VERSION = "v0.0.1" }
647             use My::Module;
648              
649             =head2 Cutting a release
650              
651             carton exec -- dzil release
652              
653             This should calculate the new version number, build a new release tarball,
654             add a release tag, create the release on GitHub and upload the tarball to it.
655              
656             You can set the C<V> environment variable to force a specific version,
657             as described by L<Dist::Zilla::Plugin::Git::NextVersion>.
658              
659             V=2.0.0 carton exec -- dzil release
660              
661             =over
662              
663             =item Make sure your local checkout has what you want to release
664              
665             Completing a C<< dzil release >> will commit any changes,
666             tag the release version to the currently checked out commit,
667             and push to the remote.
668              
669             =item Your git remote must be a format GitHub::UploadRelease understands
670              
671             Either
672             C<git@github.com:GrantsStreetGroup/$repo.git>,
673             C<ssh://git@github.com/GrantsStreetGroup/$repo.git>,
674             or
675             C<https://github.com/GrantsStreetGroup/$repo.git>.
676              
677             As shown in the "Fetch URL" from C<git remote -n $remote>,
678              
679             =item Set C<github.user> and C<github.token>
680              
681             You can get a GitHub token by following
682             L<GitHub's instructions|https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line>.
683              
684             git config --global github.user github_login_name
685             git config --global github.token token_from_instructions_above
686              
687             =back
688              
689             =head1 AUTHOR
690              
691             Grant Street Group <developers@grantstreet.com>
692              
693             =head1 COPYRIGHT AND LICENSE
694              
695             This software is Copyright (c) 2019 - 2023 by Grant Street Group.
696              
697             This is free software, licensed under:
698              
699             The Artistic License 2.0 (GPL Compatible)
700              
701             =cut