File Coverage

blib/lib/ADAMK/Release.pm
Criterion Covered Total %
statement 37 39 94.8
branch n/a
condition n/a
subroutine 13 13 100.0
pod n/a
total 50 52 96.1


line stmt bran cond sub pod time code
1             package ADAMK::Release;
2              
3 1     1   788 use 5.10.0;
  1         3  
  1         48  
4 1     1   5 use strict;
  1         2  
  1         31  
5 1     1   6 use warnings;
  1         10  
  1         30  
6 1     1   5 use Carp ();
  1         2  
  1         22  
7 1     1   958 use CPAN::Uploader 0.103003 ();
  1         96088  
  1         28  
8 1     1   4932 use Devel::PPPort 3.21 ();
  1         541  
  1         33  
9 1     1   1150 use File::Spec::Functions 0.80 ':ALL';
  1         1090  
  1         260  
10 1     1   1145 use File::Slurp 9999.19 ();
  1         5700  
  1         29  
11 1     1   2061 use File::Find::Rule 0.32 ();
  1         12122  
  1         33  
12 1     1   1352 use File::Flat 1.04 ();
  1         19060  
  1         31  
13 1     1   968 use File::ShareDir 1.03 ();
  1         8073  
  1         29  
14 1     1   905 use File::LocalizeNewlines 1.12 ();
  1         8794  
  1         28  
15 1     1   569 use GitHub::Extract 0.02 ();
  0            
  0            
16             use IO::Prompt::Tiny 0.002 ();
17             use Module::Extract::VERSION 1.01 ();
18             use Params::Util 1.00 ':ALL';
19             use Term::ReadKey 2.14 ();
20             use YAML::Tiny 1.51 ();
21              
22             our $VERSION = '0.02';
23              
24             use constant TOOLS => qw{
25             cat
26             chmod
27             make
28             touch
29             sudo
30             bash
31             };
32              
33             use Object::Tiny 1.01 qw{
34             module
35             github
36             verbose
37             release
38             no_rt
39             no_changes
40             no_copyright
41             no_test
42             }, map { "bin_$_" } TOOLS;
43              
44              
45              
46              
47              
48              
49             ######################################################################
50             # Constructor
51              
52             sub new {
53             my $self = shift->SUPER::new(@_);
54              
55             # Check module
56             unless ( _CLASS($self->module) ) {
57             $self->error("Missing or invalid module");
58             }
59              
60             # Inflate and check the github object
61             if ( Params::Util::_HASH($self->github) ) {
62             $self->{github} = GitHub::Extract->new( %{$self->github} );
63             }
64             unless ( Params::Util::_INSTANCE($self->github, 'GitHub::Extract')) {
65             $self->error("Missing or invalid GitHub specification");
66             }
67              
68             # Release options
69             $self->{release} = !!$self->{release};
70              
71             # Find all of the command line tools
72             foreach my $tool ( TOOLS ) {
73             $self->{ "bin_" . $tool } = $self->which($tool);
74             }
75              
76             return $self;
77             }
78              
79              
80              
81              
82              
83             ######################################################################
84             # Command Methods
85              
86             sub run {
87             my $self = shift;
88              
89             # Export from GitHub and change to the directory
90             my $pushd = $self->github->pushd;
91             unless ( $pushd ) {
92             $self->error(
93             "Failed to download and extract %s: %s",
94             $self->github->url,
95             $self->github->error,
96             );
97             }
98              
99             # This is total bulldozer coding, there is no reason whatsoever why
100             # this stuff should be in seperate methods except that it provides
101             # a little cleaner logical breakup, and maybe I want to subclass this
102             # someday or something.
103             $self->validate;
104             $self->assemble;
105             $self->build;
106              
107             # Release the distribution
108             $self->upload if $self->release;
109              
110             return;
111             }
112              
113             sub validate {
114             my $self = shift;
115              
116             unless ( $self->dist_version ) {
117             $self->error("Failed to find version number in main module");
118             }
119             unless ( $self->makefile_pl or $self->build_pl ) {
120             $self->error("Failed to find Makefile.PL or Build.PL");
121             }
122              
123             return;
124             }
125              
126             sub assemble {
127             my $self = shift;
128              
129             # Create MANIFEST.SKIP
130             if ( -f $self->dist_manifest_add ) {
131             $self->shell(
132             $self->bin_cat,
133             $self->shared_manifest_skip,
134             $self->dist_manifest_add,
135             '>',
136             $self->dist_manifest_skip,
137             "Failed to merge common MANIFEST.SKIP with extra one",
138             );
139              
140             } elsif ( not -f $self->dist_manifest ) {
141             $self->copy( $self->shared_manifest_skip => $self->dist_manifest_skip );
142             }
143              
144             # Apply a default LICENSE file
145             unless ( -f $self->dist_license ) {
146             $self->copy( $self->shared_license => $self->dist_license );
147             }
148              
149             # Add ppport.h if any XS files use it
150             if ( $self->find_ppport->in( $self->dist_dir ) ) {
151             Devel::PPPort::WriteFile( $self->dist_ppport );
152             }
153              
154             # Copy in author tests as needed
155             unless ( -f $self->dist_99_author ) {
156             foreach my $xt ( qw{ pod.t pmv.t } ) {
157             next if -f catfile( $self->dist_xt, $xt );
158             $self->copy(
159             catfile( $self->shared_dir, $xt ),
160             catfile( $self->dist_xt, $xt ),
161             );
162             }
163             }
164              
165             # Create the README file
166             unless ( -f $self->dist_readme ) {
167             my $dist_readme = $self->dist_readme;
168             my $module_pod = -f $self->module_pod ? $self->module_pod : $self->module_pm;
169             $self->shell(
170             $self->bin_cat,
171             $module_pod,
172             "| pod2text >",
173             $dist_readme,
174             "Error while generating README file '$dist_readme'",
175             )
176             }
177              
178             # Localise all newlines in text files
179             $self->file_localize->localize( $self->dist_dir );
180            
181             # Check for various unsafe things in Makefile.PL
182             if ( $self->makefile_pl ) {
183             if ( $self->makefile_pl =~ /use inc::Module::Install/ ) {
184             if ( $self->makefile_pl =~ /\bauto_install\b/ ) {
185             $self->error("Makefile.PL contains dangerous auto_install");
186             }
187             } else {
188             unless ( $self->makefile_pl =~ /use strict/ ) {
189             $self->error("Makefile.PL does not use strict");
190             }
191             unless ( $self->makefile_pl =~ /(?:use|require) \d/ ) {
192             $self->error("Makefile.PL does not declare a minimum Perl version");
193             }
194             }
195             }
196              
197             # Check file permissions
198             foreach my $file ( sort $self->find_0644->in( $self->dist_dir ) ) {
199             my $mode = (stat($file))[2] & 07777;
200             next if $mode == 0644;
201             $self->shell(
202             $self->bin_chmod,
203             '0644',
204             $file,
205             "Error setting $file to 0644 permissions",
206             );
207             }
208              
209             # Make sure exe files are marked with executable permissions
210             if ( $self->find_executable->in( $self->dist_dir ) ) {
211             $self->error("Found at least one .exe file without -x unix permissions");
212             }
213              
214             # Check the Changes file
215             unless ( $self->no_changes ) {
216             # Read in the Changes file
217             unless ( -f $self->dist_changes ) {
218             $self->error("Distribution does not have a Changes file");
219             }
220             unless ( open( CHANGES, $self->dist_changes ) ) {
221             $self->error("Failed to open Changes file");
222             }
223             my @lines = <CHANGES>;
224             close CHANGES;
225             unless ( @lines >= 3 ) {
226             $self->error("Changes file is empty or too small");
227             }
228              
229             # The Changes version should be the first thing on the third line
230             my $current = $lines[2];
231             my ($version) = split /\s+/, $current;
232             unless ( $version =~ /[\d\._]{3}/ ) {
233             $self->error(
234             "Failed to find current version, or too short, in '%2'",
235             $current,
236             );
237             }
238              
239             # Does it match the version in the main module
240             unless ( $version eq $self->dist_version ) {
241             $self->error(
242             "Version in Changes file (%s) does not match module version (%s)",
243             $version,
244             $self->dist_version,
245             );
246             }
247             }
248              
249             # Check that the main module documentation Copyright is the current year
250             unless ( $self->no_copyright ) {
251             # Read the file
252             unless ( open( MODULE, $self->module_doc ) ) {
253             $self->error(
254             "Failed to open '%s'",
255             $self->module_doc,
256             );
257             }
258             my @lines = <MODULE>;
259             close MODULE;
260              
261             # Look for the current year
262             my $year = 1900 + (localtime time)[5];
263             unless ( grep { /copyright/i and /$year/ } @lines ) {
264             $self->error("Missing Copyright, or does not refer to current year");
265             }
266              
267             # Merge the module to a single string
268             my $merged = join "\n", @lines;
269             unless ( $self->no_rt ) {
270             my $dist_name = $self->dist;
271             unless ( $merged =~ /L\<http\:\/\/rt\.cpan\.org\/.+?=([\w-]+)\>/ ) {
272             $self->error("Failed to find a link to the public RT queue");
273             }
274             unless ( $dist_name eq $1 ) {
275             $self->error("Expected a public link to $dist_name RT queue, but found a link to the $1 queue");
276             }
277             }
278             }
279              
280             # Touch all files to correct any potential time skews
281             foreach my $file ( $self->find_files->in( $self->dist_dir ) ) {
282             $self->shell(
283             $self->bin_touch,
284             $file,
285             "Error while touching $file to prevent clock skew",
286             );
287             }
288              
289             return;
290             }
291              
292             sub build {
293             my $self = shift;
294              
295             # Prevent environment variables from outside this script
296             # infecting the way we build things inside here.
297             local $ENV{AUTOMATED_TESTING} = '';
298             local $ENV{RELEASE_TESTING} = '';
299              
300             # Run either of the build protocols
301             if ( $self->makefile_pl ) {
302             $self->build_make;
303              
304             } elsif ( $self->build_pl ) {
305             $self->build_perl;
306              
307             } else {
308             $self->error("Module does not have a Makefile.PL or Build.PL");
309             }
310              
311             # Double check that the build produced a tarball where we expect it to be
312             unless ( -f $self->dist_tardist ) {
313             $self->error(
314             "Failed to create tardist at '%s'",
315             $self->dist_tardist,
316             );
317             }
318              
319             return;
320             }
321              
322             sub build_make {
323             my $self = shift;
324              
325             # Create the Makefile and MANIFEST
326             $self->build_makefile;
327             $self->build_makefile_manifest;
328              
329             unless ( $self->no_test ) {
330             # Test the distribution normally
331             $self->shell(
332             $self->bin_make,
333             'disttest',
334             'disttest failed',
335             );
336              
337             # Test with AUTOMATED_TESTING on
338             SCOPE: {
339             local $ENV{AUTOMATED_TESTING} = 1;
340             $self->build_makefile;
341             $self->shell(
342             $self->bin_make,
343             "disttest",
344             'disttest failed',
345             );
346             }
347              
348             # Test with RELEASE_TESTING on
349             SCOPE: {
350             local $ENV{RELEASE_TESTING} = 1;
351             $self->build_makefile;
352             $self->shell(
353             $self->bin_make,
354             "disttest",
355             'disttest failed',
356             );
357             }
358              
359             # Test with RELEASE_TESTING and root permissions.
360             # This catches bad test script assumptions in modules related
361             # to files and permissions (File::Remove, File::Flat etc).
362             SCOPE: {
363             local $ENV{RELEASE_TESTING} = 1;
364             $self->sudo(
365             qw{ perl Makefile.PL },
366             'Error while creating Makefile',
367             );
368             $self->sudo(
369             $self->bin_make,
370             "disttest",
371             'disttest failed',
372             );
373              
374             # Clean up leftover root files and rebuild from scratch
375             $self->build_realclean;
376             $self->build_makefile;
377             $self->build_makefile_manifest;
378              
379             # Run the test suite one last time to make sure we
380             # didn't break anything.
381             $self->sudo(
382             $self->bin_make,
383             "disttest",
384             'disttest failed',
385             );
386              
387             # Clean up the leftover root files again
388             $self->build_realclean;
389             }
390             }
391              
392             # Create the Makefile and MANIFEST
393             $self->build_makefile;
394             $self->build_makefile_manifest;
395              
396             # Build the tardist
397             $self->shell(
398             $self->bin_make,
399             "tardist",
400             'Error making distribution tarball',
401             );
402              
403             return;
404             }
405              
406             sub build_makefile {
407             my $self = shift;
408              
409             # Execute Makefile.PL with the current environment's perl
410             $self->shell(
411             qw{ perl Makefile.PL },
412             'Error while creating Makefile',
413             );
414              
415             # Add the build-system-specific elements to the META.yml
416             my $meta = YAML::Tiny->read( $self->dist_meta_yml );
417             return unless defined $meta;
418              
419             # Add the resources
420             my $save = 0;
421             unless ( $meta->[0]->{resources} ) {
422             $meta->[0]->{resources} = {};
423             $save = 1;
424             }
425             unless ( $meta->[0]->{resources}->{repository} ) {
426             $meta->[0]->{resources}->{repository} = $self->dist_resource_repository;
427             $save = 1;
428             }
429             if ( $save ) {
430             $meta->write( $self->dist_meta_yml );
431             }
432              
433             return;
434             }
435              
436             sub build_makefile_manifest {
437             my $self = shift;
438              
439             $self->shell(
440             $self->bin_make,
441             "manifest",
442             "Error while creating the MANIFEST",
443             );
444             }
445              
446             sub build_realclean {
447             my $self = shift;
448              
449             # Clean up the distribution (always with root)
450             $self->sudo(
451             $self->bin_make,
452             "realclean",
453             'sudo make clean failed',
454             );
455             $self->remove( $self->dist_manifest );
456             }
457              
458             sub build_perl {
459             my $self = shift;
460              
461             # Create the Build file
462             $self->shell(
463             qw{ perl Build.PL },
464             'Error while creating Makefile',
465             );
466              
467             # Create the MANIFEST file
468             $self->shell(
469             "./Build",
470             "manifest",
471             'Error while creating the MANIFEST',
472             );
473              
474             unless ( $self->no_test ) {
475             # Test the distribution normally
476             $self->shell(
477             qw{ ./Build disttest },
478             'disttest failed',
479             );
480             }
481              
482             # Build the tardist
483             $self->shell(
484             qw{ ./Build dist },
485             'Error making distribution tarball',
486             );
487              
488             return;
489             }
490              
491             sub upload {
492             my $self = shift;
493              
494             my $pauseid = $self->prompt("PAUSEID:");
495             unless (_STRING($pauseid) and $pauseid =~ /^[A-Z]{3,}$/) {
496             $self->error("Missing or invalid PAUSEID");
497             }
498              
499             my $password = $self->password("Password:");
500             unless (_STRING($password) and $password =~ /^\S{5,}$/) {
501             $self->error("Missing or invalid CPAN password");
502             }
503              
504             # Execute the upload to CPAN
505             CPAN::Uploader->upload_file( $self->dist_tardist, {
506             user => $pauseid,
507             password => $password,
508             });
509             }
510              
511              
512              
513              
514              
515             ######################################################################
516             # Content and Scanning Methods
517              
518             # Get the main github repository url for this release
519             sub dist_resource_repository {
520             my $self = shift;
521              
522             return join( '',
523             "https://github.com/",
524             $self->github->username,
525             $self->github->repository,
526             '.git',
527             );
528             }
529              
530             sub makefile_pl {
531             my $self = shift;
532             unless ( defined $self->{makefile_pl} ) {
533             my $file = $self->dist_makefile_pl;
534             return undef unless -f $file;
535             $self->{makefile_pl} = File::Slurp::read_file($file);
536             }
537             return $self->{makefile_pl};
538             }
539              
540             sub build_pl {
541             my $self = shift;
542             unless ( defined $self->{build_pl} ) {
543             my $file = $self->dist_build_pl;
544             return undef unless -f $file;
545             $self->{build_pl} = File::Slurp::read_file($file);
546             }
547             return $self->{build_pl};
548             }
549              
550             sub module_doc {
551             my $self = shift;
552             unless ( exists $self->{module_doc} ) {
553             if ( -f $self->module_pod ) {
554             $self->{module_doc} = $self->module_pod;
555             } else {
556             $self->{module_doc} = $self->module_pm;
557             }
558             }
559             return $self->{module_doc};
560             }
561              
562             sub module_version {
563             my $self = shift;
564             unless ( $self->{module_version} ) {
565             my $file = $self->module_pm;
566             my $version = Module::Extract::VERSION->parse_version_safely($file);
567             unless ( $version and $version ne 'undef' ) {
568             return undef;
569             }
570             $self->{module_version} = $version;
571             }
572             return $self->{module_version};
573             }
574              
575             sub find_ppport {
576             File::Find::Rule->name('*.xs')->file->grep(qr/\bppport\.h\b/);
577             }
578              
579             sub find_files {
580             File::Find::Rule->file;
581             }
582              
583             sub find_0644 {
584             File::Find::Rule->name(qw{
585             Changes
586             Makefile.PL
587             META.yml
588             *.t
589             *.pm
590             *.pod
591             } )->file;
592             }
593              
594             sub find_executable {
595             File::Find::Rule->name('*.exe')->not_executable->file;
596             }
597              
598             sub find_localize {
599             File::Find::Rule->file->not_binary->writable;
600             }
601              
602             sub file_localize {
603             File::LocalizeNewlines->new(
604             filter => $_[0]->find_localize,
605             verbose => 1,
606             );
607             }
608              
609              
610              
611              
612              
613             ######################################################################
614             # Paths and Files
615              
616             sub dist {
617             my $self = shift;
618             my $dist = $self->module;
619             $dist =~ s/::/-/g;
620             return $dist;
621             }
622              
623             sub dist_dir {
624             curdir();
625             }
626              
627             sub dist_tardist {
628             $_[0]->dist_file;
629             }
630              
631             sub dist_file {
632             $_[0]->dist . '-' . $_[0]->dist_version . '.tar.gz';
633             }
634              
635             sub dist_version {
636             $_[0]->module_version;
637             }
638              
639             sub dist_makefile_pl {
640             'Makefile.PL';
641             }
642              
643             sub dist_build_pl {
644             'Build.PL';
645             }
646              
647             sub dist_changes {
648             'Changes';
649             }
650              
651             sub dist_license {
652             'LICENSE';
653             }
654              
655             sub dist_readme {
656             'README';
657             }
658              
659             sub dist_meta_yml {
660             'META.yml';
661             }
662              
663             sub dist_manifest {
664             'MANIFEST';
665             }
666              
667             sub dist_manifest_skip {
668             'MANIFEST.SKIP';
669             }
670              
671             sub dist_manifest_add {
672             'MANIFEST.SKIP.add';
673             }
674              
675             sub dist_ppport {
676             'ppport.h';
677             }
678              
679             sub dist_t {
680             't';
681             }
682              
683             sub dist_data {
684             catdir('t', 'data');
685             }
686              
687             sub dist_99_author {
688             catfile('t', '99_author.t');
689             }
690              
691             sub dist_xt {
692             'xt';
693             }
694              
695             sub module_pm {
696             catfile( 'lib', $_[0]->module_subpath ) . '.pm';
697             }
698              
699             sub module_pod {
700             catfile( 'lib', $_[0]->module_subpath ) . '.pod';
701             }
702              
703             sub module_subpath {
704             catdir( split /::/, $_[0]->module );
705             }
706              
707             sub shared_manifest_skip {
708             catfile( $_[0]->shared_dir, 'MANIFEST.SKIP' );
709             }
710              
711             sub shared_license {
712             catfile( $_[0]->shared_dir, 'LICENSE' );
713             }
714              
715             sub shared_dir {
716             File::ShareDir::dist_dir('ADAMK-Release')
717             or $_[0]->error("Failed to find share directory");
718             }
719              
720              
721              
722              
723             ######################################################################
724             # Support Methods
725              
726             # Is a particular program installed, and where
727             sub which {
728             my $self = shift;
729             my $program = shift;
730             my ($location) = (`which $program`);
731             chomp $location;
732             unless ( $location ) {
733             $self->error("Can't find the required program '$program'. Please install it");
734             }
735             unless ( -r $location and -x $location ) {
736             $self->error("The required program '$program' is installed, but I do not have permission to read or execute it");
737             }
738             return $location;
739             }
740              
741             sub copy {
742             my $self = shift;
743             my $from = shift;
744             my $to = shift;
745             File::Flat->copy( $from => $to ) and return 1;
746             $self->error("Failed to copy '$from' to '$to'");
747             }
748              
749             sub move {
750             my $self = shift;
751             my $from = shift;
752             my $to = shift;
753             File::Flat->copy( $from => $to ) and return 1;
754             $self->error("Failed to move '$from' to '$to'");
755             }
756              
757             sub remove {
758             my $self = shift;
759             my $path = shift;
760             if ( -e $path ) {
761             $self->sudo(
762             "rm -rf $path",
763             "Failed to remove '$path'"
764             );
765             }
766             return 1;
767             }
768              
769             sub sudo {
770             my $self = shift;
771             my $message = pop @_;
772             my $cmd = join ' ', @_;
773             my $env = $self->env(
774             ADAMK_RELEASE => 1,
775             RELEASE_TESTING => $ENV{RELEASE_TESTING} ? 1 : 0,
776             AUTOMATED_TESTING => $ENV{AUTOMATED_TESTING} ? 1 : 0,
777             );
778             print "> (sudo) $cmd\n" if $self->verbose;
779             my $sudo = $self->bin_sudo;
780             my $rv = ! system( "$sudo bash -c '$env $cmd'" );
781             if ( $rv or ! @_ ) {
782             return $rv;
783             }
784             $self->error($message);
785             }
786              
787             sub shell {
788             my $self = shift;
789             my $message = pop @_;
790             my $cmd = join ' ', @_;
791             my $env = $self->env(
792             ADAMK_RELEASE => 1,
793             RELEASE_TESTING => $ENV{RELEASE_TESTING} ? 1 : 0,
794             AUTOMATED_TESTING => $ENV{AUTOMATED_TESTING} ? 1 : 0,
795             );
796             print "> $cmd\n" if $self->verbose;
797             my $rv = ! system( "$env $cmd" );
798             if ( $rv or ! @_ ) {
799             return $rv;
800             }
801             $self->error($message);
802             }
803              
804             sub env {
805             my $self = shift;
806             my %env = @_;
807             join ' ', map { "$_=$env{$_}" } sort keys %env;
808             }
809              
810             sub error {
811             my $self = shift;
812             my $message = sprintf(shift, @_);
813             Carp::croak($message);
814             }
815              
816             sub prompt {
817             my $self = shift;
818             return IO::Prompt::Tiny::prompt(@_);
819             }
820              
821             sub password {
822             my $self = shift;
823             my $password = undef;
824             if ( defined $_[0] ) {
825             print "$_[0] ";
826             }
827             eval {
828             Term::ReadKey::ReadMode('noecho');
829             $password = <STDIN>;
830             };
831             Term::ReadKey::ReadMode(0);
832             return undef if not defined $password;
833             chomp($password);
834             return $password;
835             }
836              
837             1;
838              
839             __END__
840              
841             =head1 NAME
842              
843             ADAMK::Release -
844              
845             =head1 DESCRIPTION
846              
847             C<ADAMK::Release> is the backend behind the C<adamk-release> script that
848             is used to build distribution tarballs for modules with the minimalist
849             repository style.
850              
851             =head1 AUTHORS
852              
853             Adam Kennedy E<lt>adamk@cpan.orgE<gt>
854              
855             =head1 SEE ALSO
856              
857             L<http://ali.as/>
858              
859             =head1 COPYRIGHT
860              
861             Copyright 2013 Adam Kennedy.
862              
863             This program is free software; you can redistribute
864             it and/or modify it under the same terms as Perl itself.
865              
866             The full text of the license can be found in the
867             LICENSE file included with this module.