File Coverage

blib/lib/Dependencies/Searcher.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Dependencies::Searcher;
2              
3 5     5   122808 use 5.010;
  5         20  
  5         208  
4 5     5   6314 use Data::Printer;
  5         306905  
  5         64  
5 5     5   924 use feature qw(say);
  5         12  
  5         577  
6             # Since 2.99 a is_core() method is available :)
7 5     5   38173 use Module::CoreList 2.99;
  5         273436  
  5         80  
8 5     5   9312 use Module::Version 'get_version';
  5         764164  
  5         363  
9 5     5   15312 use autodie;
  5         96255  
  5         46  
10 5     5   48721 use Moose;
  0            
  0            
11             use IPC::Cmd qw[can_run run];
12             use Dependencies::Searcher::AckRequester;
13             use Log::Minimal env_debug => 'LM_DEBUG';
14             use File::Stamped;
15             use IO::File;
16             use File::HomeDir;
17             use File::Spec::Functions qw(catdir catfile);
18             use Version::Compare;
19             use Path::Class;
20             use ExtUtils::Installed;
21              
22             our $VERSION = '0.064';
23              
24             =head1 NAME
25              
26             Dependencies::Searcher - Search for modules used or required by a
27             distribution and build a report that can be used as L<Carton|Carton>
28             cpanfile .
29              
30             =cut
31              
32             =head1 SYNOPSIS
33              
34             use Dependencies::Searcher;
35              
36             my $searcher = Dependencies::Searcher->new();
37             my @elements = $searcher->get_files();
38             my @uses = $searcher->get_modules($path, "use");
39             my @uniq_modules = $searcher->uniq(@uses);
40              
41             $searcher->dissociate(@uniq_modules);
42              
43             $searcher->generate_report($searcher->non_core_modules);
44              
45             # Prints to cpanfile
46             # requires 'Data::Printer', '0.35';
47             # requires Moose, '2.0602';
48             # requires IPC::Cmd;
49             # requires Module::Version;
50             # ...
51              
52             =cut
53              
54             =head1 DESCRIPTION
55              
56             Maybe you don't want to have to list all the dependencies of your Perl
57             application by hand and want an automated way to build it. Maybe you
58             forgot to do it for a long time ago. Or just during a short period.
59             Anyway, you've add lots of CPAN modules. L<Carton|Carton> is here to help you
60             manage dependencies between your development environment and
61             production, but how to keep track of the list of modules you will pass
62             to L<Carton|Carton>?
63              
64             Event if it is a no brainer to keep track of this list by adding it by
65             hand, it can be much better not to have to do it.
66              
67             You will need a tool that will check for any I<requires> or I<use> in
68             your module package, and report it into a file that could be used as an
69             input L<Carton|Carton> cpanfile. Any duplicated entry will be removed and
70             modules versions will be checked and made available. Core modules will be
71             ommited because you don't need to install them (except in some special
72             case, see C<dissociate()> documentation).
73              
74             This project has begun because it has happened to me, and I don't want
75             to search for modules to install by hand, I just want to run a simple
76             script that update the list in a convenient way. It was much more
77             longer to write the module than to search by hand so I wish it could
78             be useful for you now.
79              
80             This module is made to search dependencies for I<installed
81             distributions>, it is not supposed to manage anything else.
82              
83             =cut
84              
85             =head1 WHY ISN'T IT JUST ANOTHER MODULE::SCANDEPS ?
86              
87             Module::ScanDeps is a bi-dimentional recursive scanner: it features
88             dependencies and directories recursivity.
89              
90             Dependencies::Searcher only found direct dependencies, not
91             dependencies of dependencies, it scans recursively directories but not
92             dependencies..
93              
94             These direct dependencies are passed to the Perl toolchain (cpanminus)
95             that will take care of any recursive dependencies.
96              
97             =cut
98              
99             # Init parameters
100             has 'non_core_modules' => (
101             traits => ['Array'],
102             is => 'rw',
103             isa => 'ArrayRef[Str]',
104             default => sub { [] },
105             handles => {
106             add_non_core_module => 'push',
107             count_non_core_modules => 'count',
108             },
109             );
110              
111             has 'core_modules' => (
112             traits => ['Array'],
113             is => 'rw',
114             isa => 'ArrayRef[Str]',
115             default => sub { [] },
116             handles => {
117             add_core_module => 'push',
118             count_core_modules => 'count',
119             },
120             );
121              
122             # Log stuff here
123             local $ENV{LM_DEBUG} = 0; # 1 for debug logs, 0 for info
124              
125             my $work_path = File::HomeDir->my_data;
126             my $log_fh = File::Stamped->new(
127             pattern => catdir($work_path, "dependencies-searcher.log.%Y-%m-%d.out"),
128             );
129              
130             say("tail -vf $work_path for log");
131              
132             # Overrides Log::Minimal PRINT
133             $Log::Minimal::PRINT = sub {
134             my ( $time, $type, $message, $trace) = @_;
135             print {$log_fh} "$time [$type] $message\n";
136             };
137              
138             infof(' * * * * * * * * * * * * * * * * * * * *');
139             infof('* H E R E I S A N E W S E A R C H *');
140             infof(' * * * * * * * * * * * * * * * * * * * *');
141              
142             infof("Dependencies::Searcher $VERSION debugger init.");
143             infof("Log file available in " . $work_path);
144             # End of log init
145              
146             sub get_modules {
147             # @path contains files and directories
148             my ($self, $pattern, @path) = @_;
149              
150             debugf("Ack pattern : " . $pattern);
151              
152             # The regex add the terminal semicolon at the end of the line to
153             # make the difference between comments and code, because "use" is
154             # a word that you can find often in a POD section, more much in
155             # the beginning of line than you could think
156             $pattern = "$pattern" . qr/.+;$/;
157              
158             my @params = ('--perl', '-hi', $pattern, @path);
159             foreach my $param (@params) {
160             debugf("Param : " . $param);
161             }
162              
163             my $requester = Dependencies::Searcher::AckRequester->new();
164              
165             my $ack_path = $requester->get_path();
166             debugf("Ack path : " . $ack_path);
167              
168             my $cmd_use = $requester->build_cmd(@params);
169              
170             my @moduls = $requester->ack($cmd_use);
171             infof("Found $pattern modules : " . @moduls);
172              
173             if ( defined $moduls[0]) {
174             if ($moduls[0] =~ m/^use/ or $moduls[0] =~ m/^require/) {
175             return @moduls;
176             } else {
177             critf("Failed to retrieve modules with Ack");
178             die "Failed to retrieve modules with Ack";
179             }
180             } else {
181             say "No use or require found !";
182             }
183             }
184              
185             sub get_files {
186             my $self = shift;
187             # Path::Class functions allows a more portable module
188             my $lib_dir = dir('lib');
189             my $make_file = file('Makefile.PL');
190             my $script_dir = dir('script');
191              
192             my @structure;
193             $structure[0] = "";
194             $structure[1] = "";
195             $structure[2] = "";
196             if (-d $lib_dir) {
197              
198             $structure[0] = $lib_dir;
199              
200             } else {
201             # TODO : TEST IF THE PATH IS OK ???
202             die "Don't look like we are working on a Perl module";
203             }
204              
205             if (-f $make_file) {
206             $structure[1] = $make_file;
207             }
208              
209             if (-d $script_dir) {
210             $structure[2] = $script_dir;
211             }
212              
213             return @structure;
214             }
215              
216             # Generate a "1" when merging if one of both is empty
217             # Will be clean in make_it_real method
218             sub merge_dependencies {
219             my ($self, @uses, @requires) = @_;
220             my @merged_dependencies = (@uses, @requires);
221             infof("Merged use and require dependencies");
222             return @merged_dependencies;
223             }
224              
225             # Remove special cases that aren't need at all
226             sub make_it_real {
227             my ($self, @merged) = @_;
228             my @real_modules;
229             foreach my $module ( @merged ) {
230             push(@real_modules, $module) unless
231              
232             $module =~ m/say/
233              
234             # Describes a minimal Perl version
235             or $module =~ m/^use\s[0-9]\.[0-9]+?/
236             or $module =~ m/^use\sautodie?/
237             or $module =~ m/^use\swarnings/
238             # Kind of bug generated by merge_dependencies() when there is
239             # only one array to merge
240             or $module =~ m/^1$/
241             or $module =~ m/^use\sDependencies::Searcher/;
242             }
243             return @real_modules;
244             }
245              
246             # Clean correct lines that can't be removed
247             sub clean_everything {
248             my ($self, @dirty_modules) = @_;
249             my @clean_modules = ();
250              
251             foreach my $module ( @dirty_modules ) {
252              
253             debugf("Dirty module : " . $module);
254              
255             # remove the 'use' and the space next
256             $module =~ s{
257             use \s
258             }
259             {}xi; # Empty subtitution
260              
261             # remove the require, quotes and the space next
262             # but returns the captured module name (non-greedy)
263             # i = not case-sensitive
264             $module =~ s{
265             requires \s
266             '
267             (.*?)
268             '
269             }{$1}xi; # Note -> don't insert spaces here
270              
271             # Remove the ';' at the end of the line
272             $module =~ s/ ; //xi;
273              
274             # Remove any qw(xxxxx xxxxx) or qw[xxx xxxxx]
275             # '\(' are for real 'qw()' parenthesis not for grouping
276             # Also removes empty qw()
277             $module =~ s{
278             \s qw
279             \(
280             (\s*[A-Za-z]+(\s*[A-Za-z]*))*\s*
281             \)
282             }{}xi;
283             $module =~ s{
284             \s qw
285             \[
286             ([A-Za-z]+(_[A-Za-z]+)*(\s*[A-Za-z]*))*
287             \]
288             }
289             {}xi; # Empty subtitution
290              
291             $module =~ s{
292             \s qw
293             \/
294             (\s[A-Za-z]+(_[A-Za-z]+)*(\s*[A-Za-z]*))*\s
295             \/
296             }
297             {}xi; # Empty subtitution
298              
299             # Remove method names between quotes (those that can be used
300             # without class instantiation)
301             $module =~ s{
302             \s
303             '
304             [A-Za-z]+(_[A-Za-z]+)*
305             '
306             }
307             {}xi; # Empty subtitution
308              
309             # Remove dirty bases and quotes.
310             # This regex that substitute My::Module::Name
311             # to a "base 'My::Module::Name'" by capturing
312             # the name in a non-greedy way
313             $module =~ s{
314             base \s
315             '
316             (.*?)
317             '
318             }
319             {$1}xi;
320              
321             # Remove some warning sugar
322             $module =~ s{
323             ([a-z]+)
324             \s FATAL
325             \s =>
326             \s 'all'
327             }
328             {$1}xi;
329              
330             # Remove version numbers
331             # See "a-regex-for-version-number-parsing" :
332             # http://stackoverflow.com/questions/82064/
333             $module =~ s{
334             \s
335             (\*|\d+(\.\d+)
336             {0,2}
337             (\.\*)?)$
338             }
339             {}x;
340              
341             # Remove configuration stuff like env_debug => 'LM_DEBUG' but
342             # the quoted words have been removed before
343             $module =~ s{
344             \s
345             ([A-Za-z]+(_[A-Za-z]+)*( \s*[A-Za-z]*))*
346             \s
347             =>
348             }
349             {}xi;
350              
351             debugf("Clean module : " . $module);
352             push @clean_modules, $module;
353             }
354             return @clean_modules;
355             }
356              
357              
358             sub uniq {
359             my ($self, @many_modules) = @_;
360             my @unique_modules = ();
361             my %seen = ();
362             foreach my $element ( @many_modules ) {
363             next if $seen{ $element }++;
364             debugf("Uniq element added : " . $element);
365             push @unique_modules, $element;
366             }
367             return @unique_modules;
368             }
369              
370             sub dissociate {
371             my ($self, @common_modules) = @_;
372              
373             foreach my $nc_module (@common_modules) {
374              
375             # The old way before 2.99 corelist
376             # my $core_list_answer = `corelist $nc_module`;
377              
378             my $core_list_answer = Module::CoreList::is_core($nc_module);
379              
380             if (
381             # "$]" is Perl version
382             (exists $Module::CoreList::version{ $] }{"$nc_module"})
383             or
384             # In case module don't have a version number
385             ($core_list_answer == 1)
386             ) {
387              
388             # A module can be in core but the wanted version can be
389             # more fresh than the core one...
390             # Return the most recent version
391             my $mversion_version = get_version($nc_module);
392             # Return the corelist version
393             my $corelist_version = $Module::CoreList::version{ $] }{"$nc_module"};
394              
395             debugf("Mversion version : " . $mversion_version);
396             debugf("Corelist version : " . $corelist_version);
397              
398             # Version::Compare warns about versions numbers with '_'
399             # are 'non-numeric values'
400             $corelist_version =~ s/_/./;
401             $mversion_version =~ s/_/./;
402              
403             # It's a fix for this bug
404             # https://github.com/smonff/dependencies-searcher/issues/25
405             # Recent versions of corelist modules are not include in
406             # all Perl versions corelist
407             if (&Version::Compare::version_compare(
408             $mversion_version, $corelist_version
409             ) == 1) {
410             infof(
411             $nc_module . " version " . $mversion_version .
412             " is in use but " .
413             $corelist_version .
414             " is in core list"
415             );
416             $self->add_non_core_module($nc_module);
417             infof(
418             $nc_module .
419             " is in core but has been added to non core " .
420             "because it's a fresh core"
421             );
422             next;
423             }
424              
425             # Add to core_module
426              
427             # The old way
428             # You have to push to an array ref (Moose)
429             # http://www.perlmonks.org/?node_id=695034
430             # push @{ $self->core_modules }, $nc_module;
431              
432             # The "Moose" trait way
433             # http://metacpan.org/module/Moose::Meta::Attribute::Native::Trait::Array
434             $self->add_core_module($nc_module);
435             infof($nc_module . " is core");
436              
437             } else {
438             $self->add_non_core_module($nc_module);
439             infof($nc_module . " is not in core");
440             # push @{ $self->non_core_modules }, $nc_module;
441             }
442             }
443             }
444              
445             # Open a file handle to > cpanfile
446             sub generate_report {
447              
448             my $self = shift;
449              
450             #
451             # TODO !!! Check if the module is installed already with
452             # ExtUtils::Installed. If it it not, cry that
453             # Dependencies::Searcher is designed to be used in the complete env
454             #
455              
456             open my $cpanfile_fh, '>', 'cpanfile' or die "Can't open cpanfile : $:!";
457              
458             foreach my $module_name ( @{$self->non_core_modules} ) {
459              
460             my $version = get_version($module_name);
461              
462             # if not undef
463             if ($version) {
464             debugf("Module + version : " . $module_name . " " . $version);
465              
466             # Add the "requires $module_name\n" to the next line of the file
467             chomp($module_name, $version);
468              
469             if ($version =~ m/[0-9]\.[0-9]+/ ) {
470             say $cpanfile_fh "requires '$module_name', '$version';";
471             } # else : other case ?
472              
473             } else {
474             debugf("Module + version : " . $module_name);
475             say $cpanfile_fh "requires '$module_name';";
476             }
477              
478             }
479              
480             close $cpanfile_fh;
481             infof("File has been generated and is waiting for you");
482             }
483              
484             1;
485              
486             __END__
487              
488             =pod
489              
490             =head1 SUBROUTINES/METHODS
491              
492             =head2 get_files()
493              
494             C<get_files()> returns an array containing which file or directories has
495             been found in the current root distribution directory. We suppose it
496             can find dependancies in 3 different places :
497              
498             =over 2
499              
500             =item * files in C<lib/> directory, recursively
501              
502             =item * C<Makefile.PL>
503              
504             =item * C<script/> directory, i.e. if we use a Catalyst application
505              
506             =item * maybe it should look in C<t/> directory (todo)
507              
508             =back
509              
510             If the C<lib/> directory don't exist, the program die because we
511             consider we are not into a plain old Perl Module.
512              
513             This is work in progress, if you know other places where we can find
514             stuff, please report a bug.
515              
516             =cut
517              
518             =head2 get_modules("pattern", @elements)
519              
520             You must pass a pattern to search for, and the elements (files or
521             directories) where you want to search (array of strings from C<get_files()>).
522              
523             These patterns should be C<^use> or C<^require>.
524              
525             Then, Ack will be used to retrieve modules names into lines containing
526             patterns and return them into an array (containing also some dirt).
527             See L<Dependencies::Searcher::AckRequester> for more informations.
528              
529             =cut
530              
531             =head2 merge_dependencies(@modules, @modules)
532              
533             Simple helper method that will merge C<use> and C<require> arrays if you
534             search for both. Return an uniq array. It got a little caveat, see
535             CAVEATS.
536              
537             =cut
538              
539             =head2 make_it_real(@modules)
540              
541             Move dependencies lines from an array to an another unless it is
542             considered as a special case : minimal Perl versions, C<use autodie>,
543             C<use warnings>. These stuff has to be B<removed>. Return a I<real
544             modules> array (I<real interresting> modules).
545              
546             =cut
547              
548             =head2 clean_everything(@modules)
549              
550             After removing irrelevant stuff, we need to B<clean> what is leaving
551             and is considered as being crap (not strictly <CName::Of::Module>) but
552             needs some cleaning. We are going to remove everything but the module
553             name (even version numbers).
554              
555             This code section is well commented (because it is regex-based) so,
556             please refer to it directly.
557              
558             It returns an array of I<clean modules>.
559              
560             =cut
561              
562             =head2 uniq(@modules)
563              
564             Make each array element uniq, because one dependency can be found many
565             times. Return an array of unique modules.
566              
567             =cut
568              
569             =head2 dissociate(@modules)
570              
571             Dissociate I<core> / I<non-core> modules using the awesome
572             C<Module::Corelist::is_core method>, that search in the current Perl
573             version if the module is from Perl core or not. Note that results can
574             be different according to the environment.
575              
576             More, B<you can have two versions of the same module installed on your
577             environment> (even if you use L<local::lib|local::lib> when you
578             install a recent version of a file that has been integrated into Perl
579             core (this version hasn't necessary been merged into core).
580              
581             So C<dissociate()> checks both and compares it, to be sure that the found core
582             module is the "integrated" version, not a fresh one that you have
583             installed yourself. If it is fresh, the module is considered as a I<non-core>.
584              
585             This method don't return anything, but it stores found dependencies on the two
586             C<core_modules> and C<non_core_modules> L<Moose|Moose> attributes arrays.
587              
588             =cut
589              
590             =head2 generate_report()
591              
592             Generate the C<cpanfile> for L<Carton|Carton>, based on data contained into
593             C<core_modules> and C<non_core_modules> attributes, with optionnal
594             version number (if version number can't be found, dependency name is
595             print alone).
596              
597             Generate a hash containing the modules could be achieved. Someday.
598              
599             =cut
600              
601             =head2 Log::Minimal::PRINT override
602              
603             Just override the way Log::Minimal is used. See LOGGING AND DEBUGGING
604             for more informations.
605              
606             =cut
607              
608             =head1 LOGGING AND DEBUGGING
609              
610             This module has a very convenient logging system that use
611             L<Log::Minimal|Log::Minimal> and L<File::Stamped|File::Stamped> to
612             write to a file that you will find in the directory where local
613             applications should store their internal data for the current
614             user. This is totally portable (Thanks to Nikolay Mishin
615             (mishin)). For exemple, on a Debian-like OS :
616              
617             ~/.local/share/dependencies-searcher.[y-M-d].out
618              
619             To debug and use these logs :
620              
621             $ tail -vf ~/local/share/dependencies-searcher.[y-M-d].out
622              
623             For more information on how to configure log level, read
624             L<Log::Minimal|Log::Minimal> documentation.
625              
626             For a simple exemple on how to use it, see this blog post http://bit.ly/1lJwyX7
627              
628             =head1 CAVEATS
629              
630             =head2 Low Win32 / Cygwin support
631              
632             This module was'nt supposed to run under Win32 / Cygwin environments
633             because it was using non portable code with slashes. I hope this gets
634             better since it has been rewritten using L<Path::Class|Path::Class>
635             but it still need some testing.
636              
637             It also us-e Ack as a hack through a system command even if it was not
638             supposed to be used like that. Yes, this is dirty. Yes, I plan to change
639             things, even if Ack do the stuff proudly this way.
640              
641             Thanks to cpantesters.org community reports, things should go better and
642             better.
643              
644             =cut
645              
646             =head1 BUGS
647              
648             Please report any bugs or feature requests to
649             C<bug-dependencies-searcher at rt.cpan.org>, or through the web
650             interface at
651             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Dependencies-Searcher>.
652             I will be notified, and then you'll automatically be notified of
653             progress on your bug as I make changes.
654              
655             =head1 TODOs
656              
657             Most of the time, todos and features are on Github and Questub.
658             See https://github.com/smonff/dependencies-searcher/issues
659              
660             =head1 SUPPORT
661              
662             You can find documentation for this module with the perldoc command.
663              
664             perldoc Dependencies::Searcher
665              
666             You can also look for information at:
667              
668             See https://github.com/smonff/dependencies-searcher/
669              
670             =over 2
671              
672             =item * RT: CPAN's request tracker (report bugs here)
673              
674             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Dependencies-Searcher>
675              
676             =item * AnnoCPAN: Annotated CPAN documentation
677              
678             L<http://annocpan.org/dist/Dependencies-Searcher>
679              
680             =item * CPAN Ratings
681              
682             L<http://cpanratings.perl.org/d/Dependencies-Searcher>
683              
684             =item * Search CPAN
685              
686             L<http://search.cpan.org/dist/Dependencies-Searcher/>
687              
688             =back
689              
690             =head1 AUTHOR
691              
692             smonff, C<< <smonff at gmail.com> >>
693              
694             =head1 CONTRIBUTORS
695              
696             =over
697              
698             =item * Nikolay Mishin (mishin) helps to make it more cross-platform
699              
700             =item * Alexandr Ciornii (chorny) advises on version numbers
701              
702             =back
703              
704             =cut
705              
706             =head1 ACKNOWLEDGEMENTS
707              
708             =over
709              
710             =item * Brian D. Foy's L<Module::Extract::Use|Module::Extract::Use>
711              
712             Was the main inspiration for this one. First, I want to use it for my needs
713             but it was not recursive...
714              
715             See L<https://metacpan.org/module/Module::Extract::Use>
716              
717             =item * L<Module::CoreList|Module::CoreList>
718              
719             What modules shipped with versions of perl. I use it extensively to detect
720             if the module is from Perl Core or not.
721              
722             See L<http://perldoc.perl.org/Module/CoreList.html>
723              
724             =item * Andy Lester's Ack
725              
726             I've use it as the main source for the module. It was pure Perl so I've choose
727             it, even if Ack is not meant for being used programatically, this use do the
728             job.
729              
730             See L<http://beyondgrep.com/>
731              
732             =back
733              
734             See also :
735              
736             =over 2
737              
738             =item * https://metacpan.org/module/Perl::PrereqScanner
739              
740             =item * http://stackoverflow.com/questions/17771725/
741              
742             =item * https://metacpan.org/module/Dist::Zilla::Plugin::AutoPrereqs
743              
744             =back
745              
746             =head1 LICENSE AND COPYRIGHT
747              
748             Copyright 2013 smonff.
749              
750             This program is free software; you can redistribute it and/or modify it
751             under the terms of either: the GNU General Public License as published
752             by the Free Software Foundation; or the Artistic License.
753              
754             See L<http://dev.perl.org/licenses/> for more information.
755              
756              
757             =cut
758              
759