package Test::CheckManifest; # ABSTRACT: Check if your Manifest matches your distro use strict; use warnings; use Cwd; use Carp; use File::Spec; use File::Basename; use Test::Builder; use File::Find; our $VERSION = '1.28'; our $VERBOSE = 1; my $test = Test::Builder->new(); my $test_bool = 1; my $plan = 0; my $counter = 0; sub import { my $self = shift; my $caller = caller; my %plan = @_; for my $func ( qw( ok_manifest ) ) { no strict 'refs'; *{$caller."::".$func} = \&$func; } $test->exported_to($caller); $test->plan(%plan); $plan = 1 if(exists $plan{tests}); } sub ok_manifest{ my ($hashref,$msg) = @_; $test->plan(tests => 1) unless $plan; my $is_hashref = 1; $is_hashref = 0 unless ref($hashref); unless ( $is_hashref ) { $msg = $hashref; $hashref = {}; } my $tmp_path = dirname( File::Spec->rel2abs( $0 ) ); if ( $hashref->{file} ) { $tmp_path = dirname $hashref->{file}; } elsif ( $hashref->{dir} ) { $tmp_path = $hashref->{dir}; } my $bool = 1; my $home = Cwd::realpath( $tmp_path ); my $manifest; my $counter = 0; while ( 1 ) { my $manifest_path = File::Spec->catfile( $home . '/MANIFEST' ); last if -f $manifest_path; my $tmp_home = Cwd::realpath( File::Spec->catdir( $home, '..' ) ); last if !$tmp_home || $tmp_home eq $home || $counter++ == 20; $home = $tmp_home; } eval { $manifest = Cwd::realpath( $home . '/MANIFEST' ); 1; }; if ( !$manifest ) { $test->BAILOUT( 'Cannot find a MANIFEST. Please check!' ); } my $skip; my $skip_path = File::Spec->catfile( $home, 'MANIFEST.SKIP' ); eval { $skip = Cwd::realpath( $skip_path ) if -f $skip_path; 1; }; my @missing_files = (); my @files_plus = (); my $arref = ['/blib' , '/_build']; my $filter = $is_hashref && $hashref->{filter} ? $hashref->{filter} : []; my $comb = $is_hashref && $hashref->{bool} && $hashref->{bool} =~ m/^and$/i ? 'and' : 'or'; push @$arref, @{$hashref->{exclude}} if $is_hashref and exists $hashref->{exclude} and ref($hashref->{exclude}) eq 'ARRAY'; for(@$arref){ croak 'path in excluded array must be "absolute"' unless m!^/!; my $path = $home . $_; next unless -e $path; $_ = Cwd::realpath($path); } @$arref = grep { defined }@$arref; unless( open my $fh, '<', $manifest ){ $bool = 0; $msg = "can't open $manifest"; } else{ { # extra block to use "last" my $files_in_skip = _read_skip( $skip, \$msg, \$bool ); last unless $files_in_skip; my @files = _read_file( $fh ); close $fh; chomp @files; { local $/ = "\r"; chomp @files; } for my $tfile(@files){ $tfile = (split(/\s{2,}/,$tfile,2))[0]; next unless -e $home . '/' . $tfile; $tfile = Cwd::realpath($home . '/' . $tfile); } my (@dir_files,%files_hash,%excluded); @files_hash{@files} = (); find({no_chdir => 1, wanted => sub{ my $file = $File::Find::name; my $is_excluded = _is_excluded( $file, $arref, $filter, $comb, $files_in_skip, $home, ); push(@dir_files,Cwd::realpath($file)) if -f $file and !$is_excluded; $excluded{$file} = 1 if -f $file and $is_excluded } },$home); #use Data::Dumper; #print STDERR ">>",++$counter,":",Dumper(\@files,\@dir_files); SFILE: for my $file(@dir_files){ for my $check(@files){ if($file eq $check){ delete $files_hash{$check}; next SFILE; } } push(@missing_files,$file); $bool = 0; } delete $files_hash{$_} for keys %excluded; @files_plus = sort keys %files_hash; $bool = 0 if scalar @files_plus > 0; } # close extra block } my $diag = 'The following files are not named in the MANIFEST file: '. join(', ',@missing_files); my $plus = 'The following files are not part of distro but named in the MANIFEST file: '. join(', ',@files_plus); $test->is_num($bool,$test_bool,$msg); $test->diag($diag) if scalar @missing_files >= 1 and $test_bool == 1 and $VERBOSE; $test->diag($plus) if scalar @files_plus >= 1 and $test_bool == 1 and $VERBOSE; } sub _read_file { my ($fh) = @_; my @files; my $selftest = 0; while( my $fh_line = <$fh> ){ chomp $fh_line; $selftest++ if $fh_line =~ m{# MANIFEST for Test-CheckManifest}; next if $fh_line =~ m{ \A \s* \# }x; next if $selftest && $fh_line =~ m{# selftest}; my ($file); if ( ($file) = $fh_line =~ /^'(\\[\\']|.+)+'\s*(.*)/) { $file =~ s/\\([\\'])/$1/g; } else { ($file) = $fh_line =~ /^(\S+)\s*(.*)/; } next unless $file; push @files, $file; } return @files; } sub _not_ok_manifest{ $test_bool = 0; ok_manifest(@_); $test_bool = 1; } sub _is_excluded{ my ($file,$dirref,$filter,$bool,$files_in_skip,$home) = @_; my @excluded_files = qw( pm_to_blib Makefile META.yml Build pod2htmd.tmp pod2htmi.tmp Build.bat .cvsignore MYMETA.json MYMETA.yml ); if ( $files_in_skip and 'ARRAY' eq ref $files_in_skip ) { (my $local_file = $file) =~ s{\Q$home\E/?}{}; for my $rx ( @{$files_in_skip} ) { my $regex = qr/$rx/; return 1 if $local_file =~ $regex; } } my @matches = grep{ $file =~ /$_$/ }@excluded_files; if($bool eq 'or'){ push @matches, $file if grep{ref($_) and ref($_) eq 'Regexp' and $file =~ /$_/}@$filter; push @matches, $file if grep{$file =~ /^\Q$_\E/}@$dirref; } else{ if(grep{$file =~ /$_/ and ref($_) and ref($_) eq 'Regexp'}@$filter and grep{$file =~ /^\Q$_\E/ and not ref($_)}@$dirref){ push @matches, $file; } } return scalar @matches; } sub _read_skip { my ($skip, $msg, $bool) = @_; return [] unless $skip and -e $skip; my @files; if( -e $skip and not open my $skip_fh, '<', $skip ) { $$bool = 0; $$msg = "can't open $skip"; return; } else { @files = _read_file( $skip_fh ); } return \@files; } 1; __END__ =pod =head1 NAME Test::CheckManifest - Check if your Manifest matches your distro =head1 VERSION version 1.28 =head1 SYNOPSIS use Test::CheckManifest; ok_manifest(); =head2 EXPORT There is only one method exported: C<ok_manifest> =head1 METHODS =head2 ok_manifest [{exclude => $arref}][$msg] checks whether the Manifest file matches the distro or not. To match a distro the Manifest has to name all files that come along with the distribution. To check the Manifest file, this module searches for a file named C<MANIFEST>. To exclude some directories from this test, you can specify these dirs in the hashref. ok_manifest({exclude => ['/var/test/']}); is ok if the files in C</path/to/your/dist/var/test/> are not named in the C<MANIFEST> file. That means that the paths in the exclude array must be "pseudo-absolute" (absolute to your distribution). To use a "filter" you can use the key "filter" ok_manifest({filter => [qr/\.svn/]}); With that you can exclude all files with an '.svn' in the filename or in the path from the test. These files would be excluded (as examples): =over 4 =item * /dist/var/.svn/test =item * /dist/lib/test.svn =back You can also combine "filter" and "exclude" with 'and' or 'or' default is 'or': ok_manifest({exclude => ['/var/test'], filter => [qr/\.svn/], bool => 'and'}); These files have to be named in the C<MANIFEST>: =over 4 =item * /var/foo/.svn/any.file =item * /dist/t/file.svn =item * /var/test/test.txt =back These files not: =over 4 =item * /var/test/.svn/* =item * /var/test/file.svn =back =head1 EXCLUDING FILES Beside C<filter> and C<exclude> there is another way to exclude files: C<MANIFEST.SKIP>. This is a file with filenames that should be excluded: t/my_very_own.t file_to.skip =head1 ACKNOWLEDGEMENT Great thanks to Christopher H. Laco, who did a lot of testing stuff for me and he reported some bugs to RT. =head1 AUTHOR Renee Baecker <reneeb@cpan.org> =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2014 by Renee Baecker. This is free software, licensed under: The Artistic License 2.0 (GPL Compatible) =cut