File Coverage

blib/lib/Dist/Zilla/Plugin/MetaData/BuiltWith.pm
Criterion Covered Total %
statement 166 198 83.8
branch 45 70 64.2
condition 5 7 71.4
subroutine 26 29 89.6
pod 2 4 50.0
total 244 308 79.2


line stmt bran cond sub pod time code
1 8     8   10225024 use 5.006;
  8         20  
2 8     8   33 use strict;
  8         10  
  8         173  
3 8     8   26 use warnings;
  8         8  
  8         518  
4              
5             package Dist::Zilla::Plugin::MetaData::BuiltWith;
6              
7             our $VERSION = '1.004005';
8              
9             # ABSTRACT: Report what versions of things your distribution was built against
10              
11             our $AUTHORITY = 'cpan:KENTNL'; # AUTHORITY
12              
13 8     8   30 use Carp qw( carp croak );
  8         10  
  8         507  
14 8     8   30 use Config qw();
  8         22  
  8         143  
15 8     8   498 use Moose 2.0;
  8         292982  
  8         44  
16 8     8   34564 use Moose qw( with has around );
  8         10  
  8         27  
17 8     8   6482 use MooseX::Types::Moose qw( ArrayRef Bool Str );
  8         72418  
  8         78  
18 8     8   27942 use Module::Runtime qw( is_module_name );
  8         10  
  8         51  
19 8     8   4027 use Devel::CheckBin qw( can_run );
  8         568404  
  8         516  
20 8     8   1464 use Path::Tiny qw( path );
  8         14925  
  8         399  
21 8     8   37 use namespace::autoclean;
  8         26  
  8         78  
22             with 'Dist::Zilla::Role::FileGatherer';
23             with 'Dist::Zilla::Role::FileMunger';
24             with 'Dist::Zilla::Role::MetaProvider';
25              
26              
27              
28              
29              
30              
31              
32 6     6 1 994 sub mvp_multivalue_args { return qw( exclude include ) }
33              
34              
35              
36              
37              
38              
39              
40              
41              
42              
43             has _exclude => (
44             init_arg => 'exclude',
45             is => 'ro',
46             isa => ArrayRef,
47             default => sub { [] },
48             traits => [qw( Array )],
49             handles => { exclude => 'elements', },
50             );
51              
52              
53              
54              
55              
56              
57              
58              
59              
60              
61             has _include => (
62             init_arg => 'include',
63             is => 'ro',
64             isa => ArrayRef,
65             default => sub { [] },
66             traits => [qw( Array )],
67             handles => { include => 'elements', },
68              
69             );
70              
71              
72              
73              
74              
75              
76              
77              
78              
79             has show_config => ( is => 'ro', isa => 'Bool', default => 0 );
80              
81              
82              
83              
84              
85              
86              
87              
88              
89             has show_uname => ( is => 'ro', isa => Bool, default => 0 );
90              
91              
92              
93              
94              
95              
96              
97              
98              
99             has uname_call => ( is => 'ro', isa => Str, default => 'uname' );
100              
101              
102              
103              
104              
105              
106              
107              
108              
109             has uname_args => ( is => 'ro', isa => Str, default => '-a' );
110             has _uname_args => (
111             init_arg => undef,
112             is => 'ro',
113             isa => ArrayRef,
114             lazy_build => 1,
115             traits => [qw( Array )],
116             handles => { _all_uname_args => 'elements', },
117             );
118             has _stash_key => ( is => 'ro', isa => Str, default => 'x_BuiltWith' );
119              
120              
121              
122              
123              
124              
125              
126              
127              
128              
129              
130              
131              
132              
133              
134              
135              
136              
137              
138              
139              
140              
141              
142              
143              
144              
145              
146             has 'use_external_file' => (
147             is => 'ro',
148             lazy_build => 1,
149             );
150              
151              
152              
153              
154              
155              
156              
157              
158              
159              
160              
161              
162              
163              
164              
165              
166              
167             has 'external_file_name' => (
168             is => 'ro',
169             isa => Str,
170             lazy_build => 1,
171             );
172              
173             around dump_config => sub {
174             my ( $orig, $self, @args ) = @_;
175             my $config = $self->$orig(@args);
176             my $payload = $config->{ +__PACKAGE__ } = {};
177              
178             $payload->{show_uname} = $self->show_uname;
179             $payload->{_stash_key} = $self->_stash_key;
180             $payload->{show_config} = $self->show_config;
181             $payload->{use_external_file} = $self->use_external_file;
182             $payload->{external_file_name} = $self->external_file_name;
183              
184             if ( $self->show_uname ) {
185             $payload->{'uname'} = {
186             uname_call => $self->uname_call,
187             uname_args => $self->_uname_args,
188             };
189             }
190              
191             if ( $self->exclude ) {
192             $payload->{exclude} = [ $self->exclude ];
193             }
194             if ( $self->include ) {
195             $payload->{include} = [ $self->include ];
196             }
197              
198             ## no critic (RequireInterpolationOfMetachars)
199             # Self report when inherited.
200             $payload->{ q[$] . __PACKAGE__ . '::VERSION' } = $VERSION unless __PACKAGE__ eq ref $self;
201             $payload->{q[$Module::Metadata::VERSION]} = $Module::Metadata::VERSION if $INC{'Module/Metadata.pm'};
202             return $config;
203             };
204              
205             __PACKAGE__->meta->make_immutable;
206 8     8   3287 no Moose;
  8         12  
  8         70  
207              
208             sub _config {
209 6     6   14 my $self = shift;
210 6 100       251 return () unless $self->show_config;
211 1         5 my @interesting = qw( git_describe git_commit_id git_commit_date myarchname gccversion osname osver );
212 1         5 my $interested = {};
213 1         2 for my $key (@interesting) {
214             ## no critic (ProhibitPackageVars)
215 7 100 100     138 if ( defined $Config::Config{$key} and $Config::Config{$key} ne q{} ) {
216 3         12 $interested->{$key} = $Config::Config{$key};
217             }
218             }
219 1         7 return ( 'perl-config', $interested );
220             }
221              
222             sub _uname {
223 6     6   10 my $self = shift;
224 6 100       209 return () unless $self->show_uname;
225             {
226 1         1 my $str;
  1         1  
227 1 50       33 if ( not can_run( $self->uname_call ) ) {
228 0         0 $self->log( q[can't invoke ] . $self->uname_call . q[ on this device] );
229 0         0 return ();
230             }
231 1 50       348 last unless open my $fh, q{-|}, $self->uname_call, $self->_all_uname_args;
232 1         569 while ( my $line = <$fh> ) {
233 1         5 chomp $line;
234 1         14 $str .= $line;
235             }
236 1 50       29 last unless close $fh;
237 1         27 return ( 'uname', $str );
238              
239             }
240             ## no critic ( ProhibitPunctuationVars )
241              
242 0         0 $self->log(q{WARNING: `uname` invocation failed, omit from metadata});
243              
244 0         0 return ();
245              
246             }
247              
248             sub _build__uname_args {
249 1     1   2 my $self = shift;
250             ## no critic ( RequireDotMatchAnything RequireExtendedFormatting RequireLineBoundaryMatching )
251 1 50       34 return [ grep { defined $_ && $_ ne q{} } split /\s+/, $self->uname_args ];
  1         47  
252             }
253              
254             sub _build_use_external_file {
255 6     6   186 return;
256             }
257              
258             sub _build_external_file_name {
259 0     0   0 return 'misc/built_with.json';
260             }
261              
262              
263              
264              
265              
266             sub metadata {
267 6     6 0 29143 my ($self) = @_;
268 6 50 50     231 return {} unless 'only' eq ( $self->use_external_file || q[] );
269 0         0 return { $self->_stash_key, { external_file => $self->external_file_name }, };
270             }
271              
272             sub _get_prereq_modnames {
273 6     6   12 my ($self) = @_;
274              
275 6         11 my $modnames = {};
276              
277 6         181 my $prereqs = $self->zilla->prereqs->as_string_hash;
278             ## use critic
279 6 50       2690 if ( not %{$prereqs} ) {
  6         51  
280 0         0 $self->log(q{WARNING: No prereqs were found, probably a bug});
281 0         0 return [];
282             }
283 6         12 $self->log_debug( [ '%s phases defined: %s ', scalar keys %{$prereqs}, ( join q{,}, keys %{$prereqs} ) ] );
  6         16  
  6         34  
284              
285 6         1493 for my $phase_name ( keys %{$prereqs} ) {
  6         20  
286 6         9 my $phase_data = $prereqs->{$phase_name};
287 6 50       46 next unless defined $phase_data;
288 6         11 my $phase_deps = {};
289 6         8 for my $type ( keys %{$phase_data} ) {
  6         16  
290 6         11 my $type_data = $phase_data->{$type};
291 6 50       15 next unless defined $type_data;
292 6         9 for my $module ( keys %{$type_data} ) {
  6         17  
293 12         32 $phase_deps->{$module} = 1;
294             }
295             }
296 6         17 $self->log_debug( [ 'Prereqs for %s: %s', $phase_name, join q{,}, keys %{$phase_deps} ] );
  6         50  
297 6         1379 $modnames = { %{$modnames}, %{$phase_deps} };
  6         14  
  6         26  
298              
299             }
300 6         12 return [ sort keys %{$modnames} ];
  6         39  
301             }
302              
303             sub _detect_installed {
304 1509     1509   1740 my ( undef, $module ) = @_;
305              
306 1509 50       1977 croak('Cannot determine a version if module=undef') if not defined $module;
307              
308 1509 100       1975 return [ undef, undef ] if 'perl' eq $module;
309              
310 1508 100       2888 return [ undef, 'not a valid module name' ] if not is_module_name($module);
311              
312 1507         21768 my @pmname = split qr/::|'/, $module; ## no critic (RegularExpressions)
313 1507         2336 $pmname[-1] .= '.pm';
314              
315 1507         1110 my $path;
316 1507         1999 for my $incdir (@INC) {
317 14958 50       162531 next if ref $incdir;
318 14958         20706 my $fullpath = path( $incdir, @pmname );
319 14958 100       248703 next unless -e $fullpath;
320 537 50       12421 next if -d $fullpath;
321 537         7499 $path = $fullpath;
322 537         657 last;
323             }
324              
325 1507 100       8965 return [ undef, 'module was not found in INC' ] if not defined $path;
326              
327 537         6286 require Module::Metadata;
328 537         26453 my $mm = Module::Metadata->new_from_file( $path, collect_pod => 0 );
329 537 50       1729954 return [ undef, 'Module::MetaData could not parse ' . $path ] if not defined $mm;
330              
331 537         1492 my $v = $mm->version($module);
332 537 100       8710 return [ undef, 'Module::MetaData could not parse a version from ' . $path ] if not $v;
333              
334 457         4404 return [ $v, undef ];
335              
336             }
337              
338              
339              
340              
341              
342              
343              
344              
345              
346              
347             sub _metadata {
348 6     6   14 my ($self) = @_;
349 6         53 $self->log_debug(q{Metadata called});
350 6         1823 my $report = $self->_get_prereq_modnames();
351 6         13 $self->log_debug( 'Found mods: ' . scalar @{$report} );
  6         35  
352 6         1246 my %modtable;
353             my %failures;
354              
355 6         11 for my $module ( @{$report}, $self->include ) {
  6         268  
356 14         50 my $result = $self->_detect_installed($module);
357 14 100       68 $modtable{$module} = $result->[0] if defined $result->[0];
358 14 100       51 $failures{$module} = $result->[1] if defined $result->[1];
359             }
360              
361 6         316 for my $badmodule ( $self->exclude ) {
362 2 100       7 delete $modtable{$badmodule} if exists $modtable{$badmodule};
363 2 50       5 delete $failures{$badmodule} if exists $failures{$badmodule};
364             }
365             ## no critic ( Variables::ProhibitPunctuationVars )
366 6         11 my $perlver;
367              
368 6 50       29 if ( $] < 5.010000 ) {
369 0         0 $perlver = { %{ version->parse( version->parse($])->normal ) } };
  0         0  
370             }
371             else {
372 6         11 $perlver = { %{$^V} };
  6         37  
373             }
374              
375 6         36 my $result = {
376             modules => \%modtable,
377             perl => $perlver,
378             platform => $^O,
379             $self->_uname(),
380             $self->_config(),
381             };
382              
383 6 100       27 $result->{failures} = \%failures if keys %failures;
384              
385 6         201 return $result;
386             }
387              
388              
389              
390              
391              
392             sub gather_files {
393 6     6 0 455292 my ($self) = @_;
394              
395 6 50       229 return unless $self->use_external_file;
396              
397 0 0       0 my $type =
    0          
398             $self->external_file_name =~ /[.]json\z/msix ? 'JSON'
399             : $self->external_file_name =~ /[.]ya?ml\z/msix ? 'YAML'
400             : croak 'Cant guess file type for ' . $self->external_file_name;
401              
402 0         0 my $code;
403              
404 0 0       0 if ( 'JSON' eq $type ) {
405 0         0 require JSON::MaybeXS;
406 0         0 require Dist::Zilla::File::FromCode;
407 0         0 my $json = JSON::MaybeXS->new;
408 0         0 $json->pretty(1);
409 0         0 $json->canonical(1);
410 0         0 $json->convert_blessed(1);
411 0         0 $json->allow_blessed(1);
412             $code = sub {
413 0     0   0 local *UNIVERSAL::TO_JSON = sub { "$_[0]" };
  0         0  
414 0         0 return $json->encode( $self->_metadata );
415 0         0 };
416             }
417 0 0       0 if ( 'YAML' eq $type ) {
418 0         0 require YAML::Tiny;
419             $code = sub {
420 0     0   0 return YAML::Tiny::Dump( $self->_metadata );
421 0         0 };
422             }
423              
424             $self->add_file(
425 0         0 Dist::Zilla::File::FromCode->new(
426             name => $self->external_file_name,
427             code => $code,
428             code_return_type => 'text',
429             ),
430             );
431 0         0 return;
432             }
433              
434             sub munge_files {
435 6     6 1 379399 my ($self) = @_;
436              
437 6         14 my $munged = {};
438              
439 6 50 50     228 return if 'only' eq ( $self->use_external_file || q[] );
440              
441 6         13 for my $file ( @{ $self->zilla->files } ) {
  6         156  
442 7 100       234 if ( 'META.json' eq $file->name ) {
443 5         236 require JSON::MaybeXS;
444 5         96 require CPAN::Meta::Converter;
445 5         102 my $json = JSON::MaybeXS->new->pretty->canonical(1);
446 5         262 my $old = $file->code;
447             $file->code(
448             sub {
449 5     5   47156 my $content = $json->decode( $old->() );
450 5         17959 $content->{ $self->_stash_key } = $self->_metadata;
451 5         62 my $normal = CPAN::Meta::Converter->new($content)->convert( version => $content->{'meta-spec'}->{version} );
452 5         104373 return $json->encode($normal);
453             },
454 5         144 );
455 5         62 $munged->{'META.json'} = 1;
456 5         16 next;
457             }
458 2 100       86 if ( 'META.yml' eq $file->name ) {
459 1         36 require YAML::Tiny;
460 1         3 require CPAN::Meta::Converter;
461 1         22 my $old = $file->code;
462             $file->code(
463             sub {
464 1     1   9145 my $content = YAML::Tiny::Load( $old->() );
465 1         4782 $content->{ $self->_stash_key } = $self->_metadata;
466 1         7 my $normal = CPAN::Meta::Converter->new($content)->convert( version => $content->{'meta-spec'}->{version} );
467 1         4231 return YAML::Tiny::Dump($normal);
468             },
469 1         27 );
470 1         11 $munged->{'META.yml'} = 1;
471 1         3 next;
472             }
473             }
474 6 50       9 if ( not keys %{$munged} ) {
  6         32  
475 0         0 my $message = <<'EOF';
476             No META.* files to munge.
477             BuiltWith cannot operate without one in tree prior to it
478             EOF
479 0         0 $self->log_fatal($message);
480             }
481 6         20 return;
482             }
483              
484             1;
485              
486             __END__
487              
488             =pod
489              
490             =encoding UTF-8
491              
492             =head1 NAME
493              
494             Dist::Zilla::Plugin::MetaData::BuiltWith - Report what versions of things your distribution was built against
495              
496             =head1 VERSION
497              
498             version 1.004005
499              
500             =head1 SYNOPSIS
501              
502             [MetaData::BuiltWith]
503             include = Some::Module::Thats::Not::In::Preq
504             exclude = Some::Module::Youre::Ashamed::Of
505             show_uname = 1 ; default is 0
506             show_config = 1 ; default is 0
507             uname_call = uname ; the default
508             uname_args = -s -r -m -p ; the default is -a
509             use_external_file = only ; the default is undef
510              
511             =head1 DESCRIPTION
512              
513             This module provides extra metadata in your distribution, automatically documenting what versions of dependencies the author was
514             using at the time of release.
515              
516             This allows consumers of said distributions to be able to see a range of versions that are "known good" should they experience
517             problems.
518              
519             =head1 OPTIONS
520              
521             =head2 exclude
522              
523             Specify modules to exclude from version reporting
524              
525             exclude = Foo
526             exclude = Bar
527              
528             =head2 include
529              
530             Specify additional modules to include the version of
531              
532             include = Foo
533             include = Bar
534              
535             =head2 show_config
536              
537             Report "interesting" values from C<%Config::Config>
538              
539             show_config = 1 ; Boolean
540              
541             =head2 show_uname
542              
543             Report the output from C<uname>
544              
545             show_uname = 1 ; Boolean
546              
547             =head2 uname_call
548              
549             Specify what the system C<uname> function is called
550              
551             uname_call = uname ; String
552              
553             =head2 uname_args
554              
555             Specify arguments passed to the C<uname> call.
556              
557             uname_args = -a ; String
558              
559             =head2 use_external_file
560              
561             This option regulates the optional output to an isolated file.
562              
563             An external file will be created as long as this value is a true value.
564              
565             use_external_file = 1
566              
567             If this true value is the string C<only>, then it won't also be exported to META.yml/META.json
568              
569             use_external_file = only
570              
571             NOTE:
572              
573             This will still leave an x_BuiltWith section in your META.*, however, its much less fragile
574             and will simply be:
575              
576             x_BuiltWith: {
577             external_file: "your/path/here"
578             }
579              
580             This is mostly a compatibility pointer so any tools traversing a distributions history will know where and when to change
581             behavior.
582              
583             =head2 external_file_name
584              
585             This option controls what the external file will be called in conjunction with C<use_external_file>
586              
587             Default value is:
588              
589             misc/built_with.json
590              
591             Extensions:
592              
593             .json => JSON is used.
594             .yml => YAML is used (untested)
595             .yaml => YAML is used (untested)
596              
597             =head1 METHODS
598              
599             =head2 mvp_multivalue_args
600              
601             This module can take, as parameters, any volume of 'exclude' or 'include' arguments.
602              
603             =head2 munge_files
604              
605             This module scrapes together the name of all modules that exist in the "C<Prereqs>" section
606             that Dist::Zilla collects, and then works out what version of things you have,
607             applies the various include/exclude rules, and ships that data back to Dist::Zilla
608             via this method. See L<< C<Dist::Zilla>'s C<MetaProvider> role|Dist::Zilla::Role::MetaProvider >> for more details.
609              
610             =for Pod::Coverage metadata
611              
612             =for Pod::Coverage gather_files
613              
614             =head1 EXAMPLE OUTPUT ( C<META.json> )
615              
616             "x_BuiltWith" : {
617             "modules" : {
618             "Dist::Zilla::Role::MetaProvider" : "4.101612",
619             "File::Find" : "1.15",
620             "File::Temp" : "0.22",
621             "Module::Build" : "0.3607",
622             "Moose" : "1.07",
623             "Test::More" : "0.94"
624             },
625             "perl" : "5.012000",
626             "platform" : "MSWin32"
627             },
628              
629             =head1 AUTHOR
630              
631             Kent Fredric <kentnl@cpan.org>
632              
633             =head1 COPYRIGHT AND LICENSE
634              
635             This software is copyright (c) 2017 by Kent Fredric <kentnl@cpan.org>.
636              
637             This is free software; you can redistribute it and/or modify it under
638             the same terms as the Perl 5 programming language system itself.
639              
640             =cut