set v2 metadata (RT#88028)
[p5sagit/Module-Metadata.git] / t / lib / DistGen.pm
CommitLineData
7a4e305a 1package DistGen;
2
3use strict;
4
5use vars qw( $VERSION $VERBOSE @EXPORT_OK);
6
7$VERSION = '0.01';
8$VERBOSE = 0;
9
10use Carp;
11
12use MBTest ();
13use Cwd ();
14use File::Basename ();
15use File::Find ();
16use File::Path ();
17use File::Spec ();
18use IO::File ();
19use Tie::CPHash;
20use Data::Dumper;
21
22my $vms_mode;
23my $vms_lower_case;
24
25BEGIN {
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}
49BEGIN {
50 require Exporter;
51 *{import} = \&Exporter::import;
52 @EXPORT_OK = qw(
53 undent
54 );
55}
56
57sub undent {
58 my ($string) = @_;
59
60 my ($space) = $string =~ m/^(\s+)/;
61 $string =~ s/^$space//gm;
62
63 return($string);
64}
65
66sub 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
75END { chdir_all(MBTest->original_cwd); }
76
77sub new {
78 my $self = bless {}, shift;
79 $self->reset(@_);
80}
81
82sub 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
115sub remove {
116 my $self = shift;
117 $self->chdir_original if($self->did_chdir);
118 File::Path::rmtree( $self->dirname );
119 return $self;
120}
121
122sub 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
137sub _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
294sub _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
311sub name { shift()->{name} }
312
313sub dirname {
314 my $self = shift;
315 my $dist = $self->{distdir} || join( '-', split( /::/, $self->{name} ) );
316 return File::Spec->catdir( $self->{dir}, $dist );
317}
318
319sub _real_filename {
320 my $self = shift;
321 my $filename = shift;
322 return File::Spec->catfile( split( /\//, $filename ) );
323}
324
325sub 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
389sub 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
447sub add_file {
448 my $self = shift;
449 $self->change_file( @_ );
450}
451
452sub 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
463sub 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
488sub 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
497sub 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
504sub 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
513sub did_chdir { exists shift()->{original_dir} }
514
515########################################################################
516
517sub 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
526sub new_from_context {
527 my ($self, @args) = @_;
528 require Module::Build;
529 return Module::Build->new_from_context( quiet => 1, @args );
530}
531
532sub run_build_pl {
533 my ($self, @args) = @_;
534 require Module::Build;
535 return Module::Build->run_perl_script('Build.PL', [], [@args])
536}
537
538sub 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
5451;
546
547__END__
548
549
550=head1 NAME
551
552DistGen - 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
582A DistGen object manages a set of files in a distribution directory.
583
584The C<new()> constructor initializes the object and creates an empty
585directory for the distribution. It does not create files or chdir into
586the directory. The C<reset()> method re-initializes the object in a
587new directory with new parameters. It also does not create files or change
588the current directory.
589
590Some methods only define the target state of the distribution. They do B<not>
591make any changes to the filesystem:
592
593 add_file
594 change_file
595 change_build_pl
596 remove_file
597 revert
598
599Other methods then change the filesystem to match the target state of
600the distribution:
601
602 clean
603 regen
604 remove
605
606Other methods are provided for a convenience during testing. The
607most important is the one to enter the distribution directory:
608
609 chdir_in
610
611Additional 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
622Create a new object and an empty directory to hold the distribution's files.
623If no C<dir> option is provided, it defaults to MBTest->tmpdir, which sets
624a different temp directory for Perl core testing and CPAN testing.
625
626The 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
635The parameters are as follows.
636
637=over
638
639=item name
640
641The name of the module this distribution represents. The default is
642'Simple'. This should be a "Foo::Bar" (module) name, not a "Foo-Bar"
643dist name.
644
645=item dir
646
647The (parent) directory in which to create the distribution directory. The
648distribution will be created under this according to C<distdir> parameter
649below. 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
658The name of the distribution directory to create. Defaults to the dist form of
659C<name>, e.g. 'Foo-Bar' if C<name> is 'Foo::Bar'.
660
661=item xs
662
663If true, generates an XS based module.
664
665=item no_manifest
666
667If true, C<regen()> will not create a MANIFEST file.
668
669=back
670
671The 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
677If an XS module is generated, Simple.pm and basic.t are different and
678the following files are also added:
679
680 typemap
681 lib/Simple.xs # based on name parameter
682
683=head3 reset()
684
685The C<reset> method re-initializes the object as if it were generated
686from 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
692Note that C<$filename> should always be specified with unix-style paths,
693and are relative to the distribution root directory, e.g. C<lib/Module.pm>.
694
695No changes are made to the filesystem until the distribution is regenerated.
696
697=head3 add_file()
698
699Add a $filename containing $content to the distribution.
700
701 $dist->add_file( $filename, $content );
702
703=head3 change_file()
704
705Changes the contents of $filename to $content. No action is performed
706until the distribution is regenerated.
707
708 $dist->change_file( $filename, $content );
709
710=head3 change_build_pl()
711
712A wrapper around change_file specifically for setting Build.PL. Instead
713of file C<$content>, it takes a hash-ref of Module::Build constructor
714arguments:
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
727Retrieves the target contents of C<$filename>.
728
729 $content = $dist->get_file( $filename );
730
731=head3 remove_file()
732
733Removes C<$filename> from the distribution.
734
735 $dist->remove_file( $filename );
736
737=head3 revert()
738
739Returns the object to its initial state, or given a $filename it returns that
740file 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
747These methods immediately affect the filesystem.
748
749=head3 regen()
750
751Regenerate all missing or changed files. Also deletes any files
752flagged for removal with remove_file().
753
754 $dist->regen(clean => 1);
755
756If the optional C<clean> argument is given, it also calls C<clean>. These
757can also be chained like this, instead:
758
759 $dist->clean->regen;
760
761=head3 clean()
762
763Removes any files that are not part of the distribution.
764
765 $dist->clean;
766
767=head3 remove()
768
769Changes back to the original directory and removes the distribution
770directory (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
778This is like a more aggressive form of C<clean>. Generally, calling C<clean>
779and C<regen> should be sufficient.
780
781=head2 Changing directories
782
783=head3 chdir_in
784
785Change directory into the dist root.
786
787 $dist->chdir_in;
788
789=head3 chdir_original
790
791Returns to whatever directory you were in before chdir_in() (regardless
792of the cwd.)
793
794 $dist->chdir_original;
795
796=head2 Command-line helpers
797
798These use Module::Build->run_perl_script() to ensure that Build.PL or Build are
799run in a separate process using the current perl interpreter. (Module::Build
800is loaded on demand). They also ensure appropriate naming for operating
801systems that require a suffix for Build.
802
803=head3 run_build_pl
804
805Runs Build.PL using the current perl interpreter. Any arguments are
806passed on the command line.
807
808 $dist->run_build_pl('--quiet');
809
810=head3 run_build
811
812Runs Build using the current perl interpreter. Any arguments are
813passed on the command line.
814
815 $dist->run_build(qw/test --verbose/);
816
817=head2 Properties
818
819=head3 name()
820
821Returns the name of the distribution.
822
823 $dist->name: # e.g. Foo::Bar
824
825=head3 dirname()
826
827Returns 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
835Removes leading whitespace from a multi-line string according to the
836amount 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