File Coverage

blib/lib/Module/License/Report/CPANPLUSModule.pm
Criterion Covered Total %
statement 28 152 18.4
branch 1 58 1.7
condition 0 6 0.0
subroutine 9 26 34.6
pod 18 18 100.0
total 56 260 21.5


line stmt bran cond sub pod time code
1             package Module::License::Report::CPANPLUSModule;
2              
3 1     1   7 use warnings;
  1         1  
  1         45  
4 1     1   6 use strict;
  1         2  
  1         33  
5 1     1   6 use CPANPLUS::Internals::Constants;
  1         1  
  1         605  
6 1     1   1196 use File::Slurp qw();
  1         8379  
  1         34  
7 1     1   15 use File::Spec qw();
  1         3  
  1         21  
8 1     1   830 use Module::License::Report::Object;
  1         3  
  1         31  
9 1     1   832 use YAML qw();
  1         8910  
  1         3082  
10              
11             our $VERSION = '0.02';
12              
13             # This is a translation from CPAN "dslip" codes to Module::Build YAML codes
14             # From: http://cpan.uwinnipeg.ca/htdocs/faqs/dslip.html
15             # To: http://search.cpan.org/dist/Module-Build/lib/Module/Build.pm#license
16             my %dslip_license_abbrevs = (
17             p => 'perl',
18             g => 'gpl',
19             l => 'lgpl',
20             b => 'bsd',
21             a => 'artistic',
22             o => 'unrestricted',
23             );
24              
25              
26             ### CHANGES HERE SHOULD BE REFLECTED IN ::Object POD! ###
27             # This is an unordered list of possible sources for license information
28             # Each entry has these fields:
29             # name - Machine-readable codeword for the source - should not change ever
30             # description - Human-readable description of the source
31             # confidence - Number between 100 (high) and 0 (low)
32             # sub - Anonymous function that returns (, )
33             # Note that the filename may be undef
34             my @license_sources = (
35             {
36             name => 'META.yml',
37             description => 'META.yml license field',
38             confidence => 100,
39             sub => sub {
40             my $self = shift;
41             return $self->yml()->{license}, 'META.yml';
42             },
43             },
44             {
45             name => 'DSLIP',
46             description => 'CPAN license field',
47             confidence => 95,
48             sub => sub {
49             my $self = shift;
50             return $self->dslip()->{license}, undef;
51             },
52             },
53             {
54             name => 'Module',
55             description => 'Copyright statement in module file',
56             confidence => 50,
57             sub => sub {
58             my $self = shift;
59             my $file = $self->version_from();
60             return $self->license_from_file($file), $file;
61             },
62             },
63             {
64             name => 'POD',
65             description => 'Copyright statement in module pod file',
66             confidence => 45,
67             sub => sub {
68             my $self = shift;
69             my $file = $self->version_from_pod();
70             return $self->license_from_file($file), $file;
71             },
72             },
73             {
74             name => 'LicenseFile',
75             description => 'Copyright statement in miscellaneous file',
76             confidence => 25,
77             sub => sub {
78             my $self = shift;
79             my $file = $self->license_filename();
80             return $self->license_from_file($file), $file;
81             },
82             },
83             );
84              
85             =head1 NAME
86              
87             Module::License::Report::CPANPLUSModule - Abstraction of a CPAN module
88              
89             =head1 LICENSE
90              
91             Copyright 2005 Clotho Advanced Media, Inc.,
92              
93             This library is free software; you can redistribute it and/or modify it
94             under the same terms as Perl itself.
95              
96             =head1 SYNOPSIS
97              
98             use Module::License::Report::CPANPLUS.pm
99             use Module::License::Report::CPANPLUSModule.pm
100             my $cp = Module::License::Report::CPANPLUS->new();
101             my $module = Module::License::Report::CPANPLUSModule->new($cp, 'Foo::Bar');
102             my $license = $module->license();
103              
104             =head1 DESCRIPTION
105              
106             This is an extension of the CPANPLUS::Module API for use by
107             Module::License::Report. It's unlikely that you want to use this
108             directly.
109              
110             =head1 FUNCTIONS
111              
112             =over
113              
114             =item $pkg->new($cp, $module_name)
115              
116             The C<$cp> argument is a Module::License::Report::CPANPLUS
117             instance. The C<$module_name> should be of a form acceptable to
118             Module::License::Report::CPANPLUS::get_module().
119              
120             =cut
121              
122             sub new
123             {
124 1     1 1 3 my $pkg = shift;
125 1         2 my $cp = shift; # Module::License::Report::CPANPLUS instance
126 1         2 my $name = shift;
127              
128 1         6 my $self = bless {
129             cp => $cp,
130             name => $name,
131             mod => $cp->_module_by_name($name),
132             }, $pkg;
133            
134 1 50       3145 return $self->{mod} ? $self : ();
135             }
136              
137             =item $self->verbose()
138              
139             Returns a boolean.
140              
141             =cut
142              
143             sub verbose
144             {
145 0     0 1 0 my $self = shift;
146 0         0 return $self->{cp}->{verbose};
147             }
148              
149             =item $self->license()
150              
151             Returns a Module::License::Report::Object instance, or undef.
152              
153             =cut
154              
155             sub license
156             {
157 0     0 1 0 my $self = shift;
158              
159 0         0 _announce("Find license for $self->{name}", $self->verbose());
160 0         0 for my $source (reverse sort {$a->{confidence} <=> $b->{confidence}} @license_sources)
  0         0  
161             {
162 0         0 _announce(" Try source $source->{name}", $self->verbose());
163 0         0 my ($license, $file) = $source->{sub}($self);
164 0         0 my $result = {
165             name => $license,
166             source_file => $file,
167             source_name => $source->{name},
168             source_desc => $source->{description},
169             confidence => $source->{confidence},
170             module => $self,
171             };
172 0 0       0 if ($license)
173             {
174 0         0 return Module::License::Report::Object->new($result);
175             }
176             }
177 0         0 return;
178             }
179              
180             =item $self->license_from_file($filename)
181              
182             Searches the specified file for license and/or copyright information.
183             This uses heuristics.
184              
185             =cut
186              
187             sub license_from_file
188             {
189 0     0 1 0 my $self = shift;
190 0         0 my $licensefile = shift;
191              
192 0 0       0 if ($licensefile)
193             {
194 0         0 my $filename = File::Spec->catfile($self->extract_dir(), $licensefile);
195 0 0       0 if (-f $filename)
196             {
197 0         0 my $content = File::Slurp::read_file($filename);
198 0 0       0 if ($content =~ m/=head\d\s+(?:licen[cs]e|licensing|copyright|legal)\b(.*?)(=head\\d.*|=cut.*|)\z/ixms)
199             {
200 0         0 my $licensetext = $1;
201              
202             # Check for any of the following phrases (Change spaces to \s+)
203 0         0 my @phrases = (
204             'under the same (?:terms|license) as Perl itself',
205             );
206 0         0 my $regex = join q{|}, map {join '\\s+', split m/\s+/xms, $_} @phrases;
  0         0  
207 0 0       0 if ($licensetext =~ m/$regex/ixms)
208             {
209 0         0 return 'perl';
210             }
211             }
212             }
213             }
214              
215 0         0 return undef; ## no critic needs an explicit undef because of list context
216             }
217              
218             =item $self->yml()
219              
220             Loads and parses a C file. Returns a hashref that has,
221             minimally, a C field.
222              
223             =cut
224              
225             sub yml
226             {
227 0     0 1 0 my $self = shift;
228              
229 0 0       0 if (!$self->{yml})
230             {
231 0         0 $self->{yml} = {
232             license => undef,
233             };
234 0         0 my $filename = File::Spec->catfile($self->extract_dir(), 'META.yml');
235 0 0       0 if (-f $filename)
236             {
237 0         0 my $yaml = File::Slurp::read_file($filename);
238 0         0 my $meta = eval { YAML::Load($yaml) };
  0         0  
239 0 0       0 if (!$meta)
240             {
241 0         0 _announce('Failed to read META.yml', $self->verbose());
242             }
243             else
244             {
245 0         0 for my $key (qw(license))
246             {
247 0 0       0 if ($meta->{$key})
248             {
249 0         0 $self->{yml}->{$key} = $meta->{$key};
250             }
251             }
252             }
253             }
254             }
255 0         0 return $self->{yml};
256             }
257              
258             =item $self->dslip()
259              
260             Parses the CPAN DSLIP metadata. Returns a hashref that has,
261             minimally, a C field.
262              
263             See L for more
264             information.
265              
266             =cut
267              
268             sub dslip
269             {
270 0     0 1 0 my $self = shift;
271              
272 0 0       0 if (!$self->{dslip})
273             {
274 0         0 $self->{dslip} = {
275             license => undef,
276             };
277 0         0 my $dslip_str = $self->{mod}->dslip();
278 0 0       0 if ($dslip_str)
279             {
280 0         0 my ($devel_stage,
281             $support_level,
282             $language_used,
283             $interface_style,
284             $public_license) = $dslip_str =~ m/(.)/gxms;
285              
286 0 0       0 if ($public_license)
287             {
288 0         0 $self->{dslip}->{license} = $dslip_license_abbrevs{$public_license};
289             }
290             }
291             }
292 0         0 return $self->{dslip};
293             }
294              
295             =item $self->makefile()
296              
297             Loads and parses a C file. Returns a hashref that has,
298             minimally, a C field.
299              
300             The parsing is very simplistic.
301              
302             =cut
303              
304             sub makefile
305             {
306 0     0 1 0 my $self = shift;
307              
308 0 0       0 if (!$self->{makefile})
309             {
310 0         0 $self->{makefile} = {};
311 0         0 my $filename = File::Spec->catfile($self->extract_dir(), 'Makefile.PL');
312 0 0       0 if (-f $filename)
313             {
314 0         0 my $makefile = File::Slurp::read_file($filename);
315              
316             # Get main file from the MakeMaker command
317 0 0       0 if ($makefile =~ m/([\'\"]?)VERSION_FROM\1\s*(?:=>|,)\s*(\"[^\"]+|\'[^\']+)/xms)
318             {
319 0         0 my $module_file = substr $2, 1; # remove leading quote
320 0         0 $self->{makefile}->{version_from} = $module_file;
321             }
322             }
323             }
324 0         0 return $self->{makefile};
325             }
326              
327             =item $self->buildfile()
328              
329             Loads and parses a C file. Returns a hashref that has,
330             minimally, a C field.
331              
332             The parsing is very simplistic.
333              
334             =cut
335              
336             sub buildfile
337             {
338 0     0 1 0 my $self = shift;
339              
340 0 0       0 if (!$self->{buildfile})
341             {
342 0         0 $self->{buildfile} = {};
343 0         0 my $filename = File::Spec->catfile($self->extract_dir(), 'Build.PL');
344 0 0       0 if (-f $filename)
345             {
346 0         0 my $buildfile = File::Slurp::read_file($filename);
347              
348             # Get main file from the Module::Build constructor
349 0 0       0 if ($buildfile =~ m/([\'\"]?)module_name\1\s*(?:=>|,)\s*(\"[^\"]+|\'[^\']+)/xms)
    0          
350             {
351 0         0 my $module_name = substr $2, 1; # remove leading quote
352              
353             # This algorithm comes from Module::Build::Base::dist_version() v0.27_02
354 0         0 my $file = File::Spec->catfile('lib', split m/::/xms, $module_name) . '.pm';
355              
356 0         0 $self->{buildfile}->{version_from} = $file;
357             }
358             elsif ($buildfile =~ m/([\'\"]?)dist_version_from\1\s*(?:=>|,)\s*(\"[^\"]+|\'[^\']+)/xms)
359             {
360 0         0 my $module_file = substr $2, 1; # remove leading quote
361 0         0 $self->{buildfile}->{version_from} = $module_file;
362             }
363             }
364             }
365 0         0 return $self->{buildfile};
366             }
367              
368             =item $self->version_from()
369              
370             Returns the name of the file that has the definitive C.
371             This file might not exist.
372              
373             This relies on parsing C, C or C.
374              
375             =cut
376              
377             sub version_from
378             {
379 0     0 1 0 my $self = shift;
380              
381 0         0 my @candidates = (
382             $self->yml()->{version_from},
383             $self->buildfile()->{version_from},
384             $self->makefile()->{version_from},
385             );
386              
387 0         0 for my $filename (@candidates)
388             {
389 0 0 0     0 if ($filename && -f File::Spec->catfile($self->extract_dir(), $filename))
390             {
391 0         0 return $filename;
392             }
393             }
394 0         0 return;
395             }
396              
397             =item $self->version_from_pod()
398              
399             Returns the name of a C<.pod> file that corresponds to version_from().
400             This file might not exist.
401              
402             =cut
403              
404             sub version_from_pod
405             {
406 0     0 1 0 my $self = shift;
407              
408 0         0 my $version_from = $self->version_from();
409 0         0 my $version_pod;
410 0 0 0     0 if ($version_from && $version_from =~ m/ \.pm \z /xms)
411             {
412 0         0 ($version_pod = $version_from) =~ s/ \.pm \z /.pod/xms;
413             }
414 0         0 return $version_pod;
415             }
416              
417             =item $self->license_filename()
418              
419             Returns the name of the file that is the most likely source of license or copyright information.
420              
421             =cut
422              
423             sub license_filename
424             {
425 0     0 1 0 my $self = shift;
426              
427             # Check files that are for-sure
428 0         0 my @licenses = grep {m/\A (?:copyright|copying|license|gpl|lgpl|artistic) \b /ixms} $self->root_files();
  0         0  
429 0 0       0 if (@licenses > 0)
430             {
431 0         0 return $licenses[0];
432             }
433              
434             # Check doc files that might have copyright inline
435 0         0 foreach my $file ((grep {m/\A readme/ixms} $self->root_files()),
  0         0  
  0         0  
436             (grep {defined $_} $self->version_from(), $self->version_from_pod()))
437             {
438 0         0 my $filename = File::Spec->catfile($self->extract_dir(), $file);
439 0 0       0 if (-f $filename)
440             {
441 0         0 my $content = File::Slurp::read_file($filename);
442 0 0       0 if ($content =~ m/\b(?:licen[sc]e|licensing|copyright)\b/ixms) # [sc] is to catch a common typo
443             {
444 0         0 return $file;
445             }
446             }
447             }
448              
449 0         0 return;
450             }
451              
452             =item $self->root_files()
453              
454             Returns a list of all files in the root of the distribution directory,
455             like C, C, etc.
456              
457             =cut
458              
459             sub root_files
460             {
461 0     0 1 0 my $self = shift;
462              
463             # Get list of files in the root of the distro
464 0         0 my @files = grep {-f File::Spec->catfile($self->extract_dir(), $_)}
  0         0  
465             File::Slurp::read_dir($self->extract_dir());
466 0         0 return @files;
467             }
468              
469             =item $self->name()
470              
471             Returns the module name that was specified in the constructor.
472              
473             =cut
474              
475             sub name
476             {
477 0     0 1 0 my $self = shift;
478 0         0 return $self->{name};
479             }
480              
481             =item $self->package_name()
482              
483             Returns the name of the package, like C.
484              
485             =cut
486              
487             sub package_name
488             {
489 1     1 1 3 my $self = shift;
490 1         12 return $self->{mod}->package_name();
491             }
492              
493             =item $self->package_version()
494              
495             Returns the version of the package, like C<0.12.04_01>.
496              
497             =cut
498              
499             sub package_version
500             {
501 0     0 1   my $self = shift;
502 0           return $self->{mod}->package_version();
503             }
504              
505             =item $self->extract_dir
506              
507             Returns the path to the extracted distribution. If the distribution
508             is not yet extracted, does that first.
509              
510             =cut
511              
512             sub extract_dir
513             {
514 0     0 1   my $self = shift;
515 0           return $self->extract();
516             }
517              
518             =item $self->extract()
519              
520             Extracts the distribution archive (perhaps a C<.tar.gz> or a C<.zip>
521             file) and returns the path.
522              
523             =cut
524              
525             sub extract
526             {
527 0     0 1   my $self = shift;
528              
529 0           $self->fetch();
530 0 0         if (!$self->{mod}->status->extract)
531             {
532             #_announce('Extract module', $self->verbose());
533 0           $self->{mod}->extract;
534 0 0         if ($self->verbose)
535             {
536 0           _announce('Extracted to ' . $self->{mod}->status()->extract(), $self->verbose());
537             }
538             }
539 0           return $self->{mod}->status->extract;
540             }
541              
542             =item $self->fetch()
543              
544             Downloads the distribution from CPAN.
545              
546             =cut
547              
548             sub fetch
549             {
550 0     0 1   my $self = shift;
551              
552 0 0         if (!$self->{mod}->status->fetch)
553             {
554             #_announce('Fetch module', $self->verbose());
555 0           $self->{mod}->fetch;
556             }
557 0           return $self->{mod}->status->fetch;
558             }
559              
560             sub _announce
561             {
562 0     0     my $msg = shift;
563 0           my $verbose = shift;
564              
565 0 0         if ($verbose)
566             {
567 0           print $msg,"\n";
568             }
569 0           return;
570             }
571              
572             1;
573             __END__