add "use warnings" everywhere
[p5sagit/Module-Metadata.git] / t / lib / DistGen.pm
1 package DistGen;
2
3 use strict;
4 use warnings;
5
6 use vars qw( $VERSION $VERBOSE @EXPORT_OK);
7
8 $VERSION = '0.01';
9 $VERBOSE = 0;
10
11 use Carp;
12
13 use MBTest ();
14 use Cwd ();
15 use File::Basename ();
16 use File::Find ();
17 use File::Path ();
18 use File::Spec ();
19 use IO::File ();
20 use Tie::CPHash;
21 use Data::Dumper;
22
23 my $vms_mode;
24 my $vms_lower_case;
25
26 BEGIN {
27   $vms_mode = 0;
28   $vms_lower_case = 0;
29   if( $^O eq 'VMS' ) {
30     # For things like vmsify()
31     require VMS::Filespec;
32     VMS::Filespec->import;
33     $vms_mode = 1;
34     $vms_lower_case = 1;
35     my $vms_efs_case = 0;
36     my $unix_rpt = 0;
37     if (eval { local $SIG{__DIE__}; require VMS::Feature; }) {
38         $unix_rpt = VMS::Feature::current("filename_unix_report");
39         $vms_efs_case = VMS::Feature::current("efs_case_preserve");
40     } else {
41         my $env_unix_rpt = $ENV{'DECC$FILENAME_UNIX_REPORT'} || '';
42         $unix_rpt = $env_unix_rpt =~ /^[ET1]/i;
43         my $efs_case = $ENV{'DECC$EFS_CASE_PRESERVE'} || '';
44         $vms_efs_case = $efs_case =~ /^[ET1]/i;
45     }
46     $vms_mode = 0 if $unix_rpt;
47     $vms_lower_case = 0 if $vms_efs_case;
48   }
49 }
50 BEGIN {
51   require Exporter;
52   *{import} = \&Exporter::import;
53   @EXPORT_OK = qw(
54     undent
55   );
56 }
57
58 sub undent {
59   my ($string) = @_;
60
61   my ($space) = $string =~ m/^(\s+)/;
62   $string =~ s/^$space//gm;
63
64   return($string);
65 }
66
67 sub chdir_all ($) {
68   # OS/2 has "current directory per disk", undeletable;
69   # doing chdir() to another disk won't change cur-dir of initial disk...
70   chdir('/') if $^O eq 'os2';
71   chdir shift;
72 }
73
74 ########################################################################
75
76 END { chdir_all(MBTest->original_cwd); }
77
78 sub new {
79   my $self = bless {}, shift;
80   $self->reset(@_);
81 }
82
83 sub reset {
84   my $self = shift;
85   my %options = @_;
86
87   $options{name} ||= 'Simple';
88   $options{dir} = File::Spec->rel2abs(
89     defined $options{dir} ? $options{dir} : MBTest->tmpdir
90   );
91
92   my %data = (
93     no_manifest   => 0,
94     xs            => 0,
95     inc           => 0,
96     %options,
97   );
98   %$self = %data;
99
100   tie %{$self->{filedata}}, 'Tie::CPHash';
101
102   tie %{$self->{pending}{change}}, 'Tie::CPHash';
103
104   # start with a fresh, empty directory
105   if ( -d $self->dirname ) {
106     warn "Warning: Removing existing directory '@{[$self->dirname]}'\n";
107     File::Path::rmtree( $self->dirname );
108   }
109   File::Path::mkpath( $self->dirname );
110
111   $self->_gen_default_filedata();
112
113   return $self;
114 }
115
116 sub remove {
117   my $self = shift;
118   $self->chdir_original if($self->did_chdir);
119   File::Path::rmtree( $self->dirname );
120   return $self;
121 }
122
123 sub revert {
124   my ($self, $file) = @_;
125   if ( defined $file ) {
126     delete $self->{filedata}{$file};
127     delete $self->{pending}{$_}{$file} for qw/change remove/;
128   }
129   else {
130     delete $self->{filedata}{$_} for keys %{ $self->{filedata} };
131     for my $pend ( qw/change remove/ ) {
132       delete $self->{pending}{$pend}{$_} for keys %{ $self->{pending}{$pend} };
133     }
134   }
135   $self->_gen_default_filedata;
136 }
137
138 sub _gen_default_filedata {
139   my $self = shift;
140
141   # TODO maybe a public method like this (but with a better name?)
142   my $add_unless = sub {
143     my $self = shift;
144     my ($member, $data) = @_;
145     $self->add_file($member, $data) unless($self->{filedata}{$member});
146   };
147
148   if ( ! $self->{inc} ) {
149     $self->$add_unless('Build.PL', undent(<<"      ---"));
150       use strict;
151       use Module::Build;
152
153       my \$builder = Module::Build->new(
154           module_name         => '$self->{name}',
155           license             => 'perl',
156       );
157
158       \$builder->create_build_script();
159       ---
160   }
161   else {
162     $self->$add_unless('Build.PL', undent(<<"      ---"));
163       use strict;
164       use inc::latest 'Module::Build';
165
166       my \$builder = Module::Build->new(
167           module_name         => '$self->{name}',
168           license             => 'perl',
169       );
170
171       \$builder->create_build_script();
172       ---
173   }
174
175   my $module_filename =
176     join( '/', ('lib', split(/::/, $self->{name})) ) . '.pm';
177
178   unless ( $self->{xs} ) {
179     $self->$add_unless($module_filename, undent(<<"      ---"));
180       package $self->{name};
181
182       use vars qw( \$VERSION );
183       \$VERSION = '0.01';
184
185       use strict;
186       use warnings;
187
188       1;
189
190       __END__
191
192       =head1 NAME
193
194       $self->{name} - Perl extension for blah blah blah
195
196       =head1 DESCRIPTION
197
198       Stub documentation for $self->{name}.
199
200       =head1 AUTHOR
201
202       A. U. Thor, a.u.thor\@a.galaxy.far.far.away
203
204       =cut
205       ---
206
207   $self->$add_unless('t/basic.t', undent(<<"    ---"));
208     use Test::More tests => 1;
209     use strict;
210     use warnings;
211
212     use $self->{name};
213     ok 1;
214     ---
215
216   } else {
217     $self->$add_unless($module_filename, undent(<<"      ---"));
218       package $self->{name};
219
220       \$VERSION = '0.01';
221
222       require Exporter;
223       require DynaLoader;
224
225       \@ISA = qw(Exporter DynaLoader);
226       \@EXPORT_OK = qw( okay );
227
228       bootstrap $self->{name} \$VERSION;
229
230       1;
231
232       __END__
233
234       =head1 NAME
235
236       $self->{name} - Perl extension for blah blah blah
237
238       =head1 DESCRIPTION
239
240       Stub documentation for $self->{name}.
241
242       =head1 AUTHOR
243
244       A. U. Thor, a.u.thor\@a.galaxy.far.far.away
245
246       =cut
247       ---
248
249     my $xs_filename =
250       join( '/', ('lib', split(/::/, $self->{name})) ) . '.xs';
251     $self->$add_unless($xs_filename, undent(<<"      ---"));
252       #include "EXTERN.h"
253       #include "perl.h"
254       #include "XSUB.h"
255
256       MODULE = $self->{name}         PACKAGE = $self->{name}
257
258       SV *
259       okay()
260           CODE:
261               RETVAL = newSVpv( "ok", 0 );
262           OUTPUT:
263               RETVAL
264
265       const char *
266       xs_version()
267           CODE:
268         RETVAL = XS_VERSION;
269           OUTPUT:
270         RETVAL
271
272       const char *
273       version()
274           CODE:
275         RETVAL = VERSION;
276           OUTPUT:
277         RETVAL
278       ---
279
280   # 5.6 is missing const char * in its typemap
281   $self->$add_unless('typemap', undent(<<"      ---"));
282       const char *\tT_PV
283       ---
284
285   $self->$add_unless('t/basic.t', undent(<<"    ---"));
286     use Test::More tests => 2;
287     use strict;
288
289     use $self->{name};
290     ok 1;
291
292     ok( $self->{name}::okay() eq 'ok' );
293     ---
294   }
295 }
296
297 sub _gen_manifest {
298   my $self     = shift;
299   my $manifest = shift;
300
301   my $fh = IO::File->new( ">$manifest" ) or do {
302     die "Can't write '$manifest'\n";
303   };
304
305   my @files = ( 'MANIFEST', keys %{$self->{filedata}} );
306   my $data = join( "\n", sort @files ) . "\n";
307   print $fh $data;
308   close( $fh );
309
310   $self->{filedata}{MANIFEST} = $data;
311   $self->{pending}{change}{MANIFEST} = 1;
312 }
313
314 sub name { shift()->{name} }
315
316 sub dirname {
317   my $self = shift;
318   my $dist = $self->{distdir} || join( '-', split( /::/, $self->{name} ) );
319   return File::Spec->catdir( $self->{dir}, $dist );
320 }
321
322 sub _real_filename {
323   my $self = shift;
324   my $filename = shift;
325   return File::Spec->catfile( split( /\//, $filename ) );
326 }
327
328 sub regen {
329   my $self = shift;
330   my %opts = @_;
331
332   my $dist_dirname = $self->dirname;
333
334   if ( $opts{clean} ) {
335     $self->clean() if -d $dist_dirname;
336   } else {
337     # TODO: This might leave dangling directories; e.g. if the removed file
338     # is 'lib/Simple/Simon.pm', the directory 'lib/Simple' will be left
339     # even if there are no files left in it. However, clean() will remove it.
340     my @files = keys %{$self->{pending}{remove}};
341     foreach my $file ( @files ) {
342       my $real_filename = $self->_real_filename( $file );
343       my $fullname = File::Spec->catfile( $dist_dirname, $real_filename );
344       if ( -e $fullname ) {
345         1 while unlink( $fullname );
346       }
347       print "Unlinking pending file '$file'\n" if $VERBOSE;
348       delete( $self->{pending}{remove}{$file} );
349     }
350   }
351
352   foreach my $file ( keys( %{$self->{filedata}} ) ) {
353     my $real_filename = $self->_real_filename( $file );
354     my $fullname = File::Spec->catfile( $dist_dirname, $real_filename );
355
356     if  ( ! -e $fullname ||
357         (   -e $fullname && $self->{pending}{change}{$file} ) ) {
358
359       print "Changed file '$file'.\n" if $VERBOSE;
360
361       my $dirname = File::Basename::dirname( $fullname );
362       unless ( -d $dirname ) {
363         File::Path::mkpath( $dirname ) or do {
364           die "Can't create '$dirname'\n";
365         };
366       }
367
368       if ( -e $fullname ) {
369         1 while unlink( $fullname );
370       }
371
372       my $fh = IO::File->new(">$fullname") or do {
373         die "Can't write '$fullname'\n";
374       };
375       print $fh $self->{filedata}{$file};
376       close( $fh );
377     }
378
379     delete( $self->{pending}{change}{$file} );
380   }
381
382   my $manifest = File::Spec->catfile( $dist_dirname, 'MANIFEST' );
383   unless ( $self->{no_manifest} ) {
384     if ( -e $manifest ) {
385       1 while unlink( $manifest );
386     }
387     $self->_gen_manifest( $manifest );
388   }
389   return $self;
390 }
391
392 sub clean {
393   my $self = shift;
394
395   my $here  = Cwd::abs_path();
396   my $there = File::Spec->rel2abs( $self->dirname() );
397
398   if ( -d $there ) {
399     chdir( $there ) or die "Can't change directory to '$there'\n";
400   } else {
401     die "Distribution not found in '$there'\n";
402   }
403
404   my %names;
405   tie %names, 'Tie::CPHash';
406   foreach my $file ( keys %{$self->{filedata}} ) {
407     my $filename = $self->_real_filename( $file );
408     $filename = lc($filename) if $vms_lower_case;
409     my $dirname = File::Basename::dirname( $filename );
410
411     $names{$filename} = 0;
412
413     print "Splitting '$dirname'\n" if $VERBOSE;
414     my @dirs = File::Spec->splitdir( $dirname );
415     while ( @dirs ) {
416       my $dir = ( scalar(@dirs) == 1
417                   ? $dirname
418                   : File::Spec->catdir( @dirs ) );
419       if (length $dir) {
420         print "Setting directory name '$dir' in \%names\n" if $VERBOSE;
421         $names{$dir} = 0;
422       }
423       pop( @dirs );
424     }
425   }
426
427   File::Find::finddepth( sub {
428     my $name = File::Spec->canonpath( $File::Find::name );
429
430     if ($vms_mode) {
431         if ($name ne '.') {
432             $name =~ s/\.\z//;
433             $name = vmspath($name) if -d $name;
434         }
435     }
436     if ($^O eq 'VMS') {
437         $name = File::Spec->rel2abs($name) if $name eq File::Spec->curdir();
438     }
439
440     if ( not exists $names{$name} ) {
441       print "Removing '$name'\n" if $VERBOSE;
442       File::Path::rmtree( $_ );
443     }
444   }, ($^O eq 'VMS' ? './' : File::Spec->curdir) );
445
446   chdir_all( $here );
447   return $self;
448 }
449
450 sub add_file {
451   my $self = shift;
452   $self->change_file( @_ );
453 }
454
455 sub remove_file {
456   my $self = shift;
457   my $file = shift;
458   unless ( exists $self->{filedata}{$file} ) {
459     warn "Can't remove '$file': It does not exist.\n" if $VERBOSE;
460   }
461   delete( $self->{filedata}{$file} );
462   $self->{pending}{remove}{$file} = 1;
463   return $self;
464 }
465
466 sub change_build_pl {
467   my ($self, @opts) = @_;
468
469   my $opts = ref $opts[0] eq 'HASH' ? $opts[0] : { @opts };
470
471   local $Data::Dumper::Terse = 1;
472   (my $args = Dumper($opts)) =~ s/^\s*\{|\}\s*$//g;
473
474   $self->change_file( 'Build.PL', undent(<<"    ---") );
475     use strict;
476     use warnings;
477     use Module::Build;
478     my \$b = Module::Build->new(
479     # Some CPANPLUS::Dist::Build versions need to allow mismatches
480     # On logic: thanks to Module::Install, CPAN.pm must set both keys, but
481     # CPANPLUS sets only the one
482     allow_mb_mismatch => (
483       \$ENV{PERL5_CPANPLUS_IS_RUNNING} && ! \$ENV{PERL5_CPAN_IS_RUNNING} ? 1 : 0
484     ),
485     $args
486     );
487     \$b->create_build_script();
488     ---
489   return $self;
490 }
491
492 sub change_file {
493   my $self = shift;
494   my $file = shift;
495   my $data = shift;
496   $self->{filedata}{$file} = $data;
497   $self->{pending}{change}{$file} = 1;
498   return $self;
499 }
500
501 sub get_file {
502   my $self = shift;
503   my $file = shift;
504   exists($self->{filedata}{$file}) or croak("no such entry: '$file'");
505   return $self->{filedata}{$file};
506 }
507
508 sub chdir_in {
509   my $self = shift;
510   $self->{original_dir} ||= Cwd::cwd; # only once!
511   my $dir = $self->dirname;
512   chdir($dir) or die "Can't chdir to '$dir': $!";
513   return $self;
514 }
515 ########################################################################
516
517 sub did_chdir { exists shift()->{original_dir} }
518
519 ########################################################################
520
521 sub chdir_original {
522   my $self = shift;
523
524   my $dir = delete $self->{original_dir};
525   chdir_all($dir) or die "Can't chdir to '$dir': $!";
526   return $self;
527 }
528 ########################################################################
529
530 sub new_from_context {
531   my ($self, @args) = @_;
532   require Module::Build;
533   return Module::Build->new_from_context( quiet => 1, @args );
534 }
535
536 sub run_build_pl {
537   my ($self, @args) = @_;
538   require Module::Build;
539   return Module::Build->run_perl_script('Build.PL', [], [@args])
540 }
541
542 sub run_build {
543   my ($self, @args) = @_;
544   require Module::Build;
545   my $build_script = $^O eq 'VMS' ? 'Build.com' : 'Build';
546   return Module::Build->run_perl_script($build_script, [], [@args])
547 }
548
549 1;
550
551 __END__
552
553
554 =head1 NAME
555
556 DistGen - Creates simple distributions for testing.
557
558 =head1 SYNOPSIS
559
560   use DistGen;
561
562   # create distribution and prepare to test
563   my $dist = DistGen->new(name => 'Foo::Bar');
564   $dist->chdir_in;
565
566   # change distribution files
567   $dist->add_file('t/some_test.t', $contents);
568   $dist->change_file('MANIFEST.SKIP', $new_contents);
569   $dist->remove_file('t/some_test.t');
570   $dist->regen;
571
572   # undo changes and clean up extraneous files
573   $dist->revert;
574   $dist->clean;
575
576   # exercise the command-line interface
577   $dist->run_build_pl();
578   $dist->run_build('test');
579
580   # start over as a new distribution
581   $dist->reset( name => 'Foo::Bar', xs => 1 );
582   $dist->chdir_in;
583
584 =head1 USAGE
585
586 A DistGen object manages a set of files in a distribution directory.
587
588 The C<new()> constructor initializes the object and creates an empty
589 directory for the distribution. It does not create files or chdir into
590 the directory.  The C<reset()> method re-initializes the object in a
591 new directory with new parameters.  It also does not create files or change
592 the current directory.
593
594 Some methods only define the target state of the distribution.  They do B<not>
595 make any changes to the filesystem:
596
597   add_file
598   change_file
599   change_build_pl
600   remove_file
601   revert
602
603 Other methods then change the filesystem to match the target state of
604 the distribution:
605
606   clean
607   regen
608   remove
609
610 Other methods are provided for a convenience during testing. The
611 most important is the one to enter the distribution directory:
612
613   chdir_in
614
615 Additional methods portably encapsulate running Build.PL and Build:
616
617   run_build_pl
618   run_build
619
620 =head1 API
621
622 =head2 Constructors
623
624 =head3 new()
625
626 Create a new object and an empty directory to hold the distribution's files.
627 If no C<dir> option is provided, it defaults to MBTest->tmpdir, which sets
628 a different temp directory for Perl core testing and CPAN testing.
629
630 The C<new> method does not write any files -- see L</regen()> below.
631
632   my $dist = DistGen->new(
633     name        => 'Foo::Bar',
634     dir         => MBTest->tmpdir,
635     xs          => 1,
636     no_manifest => 0,
637   );
638
639 The parameters are as follows.
640
641 =over
642
643 =item name
644
645 The name of the module this distribution represents. The default is
646 'Simple'.  This should be a "Foo::Bar" (module) name, not a "Foo-Bar"
647 dist name.
648
649 =item dir
650
651 The (parent) directory in which to create the distribution directory.  The
652 distribution will be created under this according to C<distdir> parameter
653 below.  Defaults to a temporary directory.
654
655   $dist = DistGen->new( dir => '/tmp/MB-test' );
656   $dist->regen;
657
658   # distribution files have been created in /tmp/MB-test/Simple
659
660 =item distdir
661
662 The name of the distribution directory to create.  Defaults to the dist form of
663 C<name>, e.g. 'Foo-Bar' if C<name> is 'Foo::Bar'.
664
665 =item xs
666
667 If true, generates an XS based module.
668
669 =item no_manifest
670
671 If true, C<regen()> will not create a MANIFEST file.
672
673 =back
674
675 The following files are added as part of the default distribution:
676
677   Build.PL
678   lib/Simple.pm # based on name parameter
679   t/basic.t
680
681 If an XS module is generated, Simple.pm and basic.t are different and
682 the following files are also added:
683
684   typemap
685   lib/Simple.xs # based on name parameter
686
687 =head3 reset()
688
689 The C<reset> method re-initializes the object as if it were generated
690 from a fresh call to C<new>.  It takes the same optional parameters as C<new>.
691
692   $dist->reset( name => 'Foo::Bar', xs => 0 );
693
694 =head2 Adding and editing files
695
696 Note that C<$filename> should always be specified with unix-style paths,
697 and are relative to the distribution root directory, e.g. C<lib/Module.pm>.
698
699 No changes are made to the filesystem until the distribution is regenerated.
700
701 =head3 add_file()
702
703 Add a $filename containing $content to the distribution.
704
705   $dist->add_file( $filename, $content );
706
707 =head3 change_file()
708
709 Changes the contents of $filename to $content. No action is performed
710 until the distribution is regenerated.
711
712   $dist->change_file( $filename, $content );
713
714 =head3 change_build_pl()
715
716 A wrapper around change_file specifically for setting Build.PL.  Instead
717 of file C<$content>, it takes a hash-ref of Module::Build constructor
718 arguments:
719
720   $dist->change_build_pl(
721     {
722       module_name         => $dist->name,
723       dist_version        => '3.14159265',
724       license             => 'perl',
725       create_readme       => 1,
726     }
727   );
728
729 =head3 get_file
730
731 Retrieves the target contents of C<$filename>.
732
733   $content = $dist->get_file( $filename );
734
735 =head3 remove_file()
736
737 Removes C<$filename> from the distribution.
738
739   $dist->remove_file( $filename );
740
741 =head3 revert()
742
743 Returns the object to its initial state, or given a $filename it returns that
744 file to its initial state if it is one of the built-in files.
745
746   $dist->revert;
747   $dist->revert($filename);
748
749 =head2 Changing the distribution directory
750
751 These methods immediately affect the filesystem.
752
753 =head3 regen()
754
755 Regenerate all missing or changed files.  Also deletes any files
756 flagged for removal with remove_file().
757
758   $dist->regen(clean => 1);
759
760 If the optional C<clean> argument is given, it also calls C<clean>.  These
761 can also be chained like this, instead:
762
763   $dist->clean->regen;
764
765 =head3 clean()
766
767 Removes any files that are not part of the distribution.
768
769   $dist->clean;
770
771 =head3 remove()
772
773 Changes back to the original directory and removes the distribution
774 directory (but not the temporary directory set during C<new()>).
775
776   $dist = DistGen->new->chdir->regen;
777   # ... do some testing ...
778
779   $dist->remove->chdir_in->regen;
780   # ... do more testing ...
781
782 This is like a more aggressive form of C<clean>.  Generally, calling C<clean>
783 and C<regen> should be sufficient.
784
785 =head2 Changing directories
786
787 =head3 chdir_in
788
789 Change directory into the dist root.
790
791   $dist->chdir_in;
792
793 =head3 chdir_original
794
795 Returns to whatever directory you were in before chdir_in() (regardless
796 of the cwd.)
797
798   $dist->chdir_original;
799
800 =head2 Command-line helpers
801
802 These use Module::Build->run_perl_script() to ensure that Build.PL or Build are
803 run in a separate process using the current perl interpreter.  (Module::Build
804 is loaded on demand).  They also ensure appropriate naming for operating
805 systems that require a suffix for Build.
806
807 =head3 run_build_pl
808
809 Runs Build.PL using the current perl interpreter.  Any arguments are
810 passed on the command line.
811
812   $dist->run_build_pl('--quiet');
813
814 =head3 run_build
815
816 Runs Build using the current perl interpreter.  Any arguments are
817 passed on the command line.
818
819   $dist->run_build(qw/test --verbose/);
820
821 =head2 Properties
822
823 =head3 name()
824
825 Returns the name of the distribution.
826
827   $dist->name: # e.g. Foo::Bar
828
829 =head3 dirname()
830
831 Returns the directory where the distribution is created.
832
833   $dist->dirname; # e.g. t/_tmp/Simple
834
835 =head2 Functions
836
837 =head3 undent()
838
839 Removes leading whitespace from a multi-line string according to the
840 amount of whitespace on the first line.
841
842   my $string = undent("  foo(\n    bar => 'baz'\n  )");
843   $string eq "foo(
844     bar => 'baz'
845   )";
846
847 =cut
848
849 # vim:ts=2:sw=2:et:sta