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