File Coverage

blib/lib/Dist/Zilla/Plugin/SyncCPANfile.pm
Criterion Covered Total %
statement 72 73 98.6
branch 26 28 92.8
condition 13 16 81.2
subroutine 11 11 100.0
pod 0 2 0.0
total 122 130 93.8


line stmt bran cond sub pod time code
1             package Dist::Zilla::Plugin::SyncCPANfile;
2              
3             # ABSTRACT: Sync a cpanfile with the prereqs listed in dist.ini
4              
5             #use v5.10;
6              
7 6     6   10673208 use strict;
  6         38  
  6         179  
8 6     6   39 use warnings;
  6         13  
  6         259  
9              
10             our $VERSION = '0.04'; # VERSION
11              
12 6     6   36 use version;
  6         28  
  6         50  
13              
14 6     6   2006 use Moose;
  6         1191379  
  6         55  
15 6     6   39929 use namespace::autoclean;
  6         15  
  6         62  
16 6     6   453 use Path::Tiny;
  6         17  
  6         432  
17 6     6   3190 use CPAN::Audit;
  6         1437165  
  6         5261  
18              
19             with qw(
20             Dist::Zilla::Role::AfterBuild
21             );
22              
23             has cpan_audit => (
24             is => 'ro',
25             isa => 'Bool',
26             default => 0,
27             );
28              
29             has filename => (
30             is => 'ro',
31             isa => 'Str',
32             default => 'cpanfile',
33             );
34            
35             has comment => (
36             is => 'ro',
37             isa => 'ArrayRef[Str]',
38             default => sub {
39             [
40             ( sprintf 'This file is generated by %s v%s', __PACKAGE__, __PACKAGE__->VERSION // '<internal>' ),
41             'Do not edit this file directly. To change prereqs, edit the `dist.ini` file.',
42             ]
43             }
44             );
45              
46 17     17 0 8200377 sub mvp_multivalue_args { qw( comment ) }
47            
48             sub after_build {
49 17     17 0 1727997 my ($self) = @_;
50              
51 17         101 my $content = $self->_get_cpanfile();
52              
53             # need to write it to disk if we're in a
54             # phase that is not filemunge
55 17         2192 path( $self->filename )->spew_raw( $content );
56             }
57              
58             sub _get_cpanfile {
59 17     17   54 my ($self) = @_;
60              
61 17         169 my $audit = CPAN::Audit->new;
62              
63 17         268451 my $zilla = $self->zilla;
64 17         633 my $prereqs = $zilla->prereqs;
65            
66 17         246 my @types = qw(requires recommends suggests conflicts);
67 17         77 my @phases = qw(runtime build test configure develop);
68            
69 17         46 my $str = join "\n", ( map { "# $_" } @{ $self->comment } ), '', '';
  32         216  
  17         629  
70 17         114 for my $phase (@phases) {
71 85 100       678 my $prefix = $phase eq 'runtime' ? '' : (sprintf "\non '%s' => sub {\n", $phase );
72 85 100       191 my $postfix = $phase eq 'runtime' ? '' : "};\n";
73 85 100       202 my $indent = $phase eq 'runtime' ? '' : ' ';
74              
75 85         146 for my $type (@types) {
76 340         1951 my $req = $prereqs->requirements_for($phase, $type);
77              
78 340 100       27110 next unless $req->required_modules;
79              
80 17         167 $str .= $prefix;
81            
82 17         58 for my $module ( sort $req->required_modules ) {
83 19   100     194 my $version = $req->requirements_for_module( $module ) || 0;
84              
85 19         1318 my ($min_version, $advisories);
86              
87 19 100       757 if ( $self->cpan_audit ) {
88 9         53 ($min_version, $advisories) = _audit( $audit, $module, $version );
89             }
90              
91 19 100 100     206 if ( $advisories && $version =~ m{(>|<|>=|<=|!=|==)} ) {
    100          
92              
93             # this seems to be a version range, so check if the latest fixed version would be accepted
94 5 100 100     57 if ( defined $min_version && !$req->accepts_module( $module, $min_version ) ) {
    100          
95 2         164 $self->log( "Range '$version' for $module does not include latest fixed version ($min_version)!" );
96             }
97             elsif ( defined $min_version ) {
98 2         153 $self->log( "Current version range includes vulnerable versions. Consider updating the minimum to $min_version" ) #if $affected_version_allowed;
99             }
100             }
101             elsif ( $advisories ) {
102              
103             # this branch is used when no version range is given but a version number
104 3   33     66 my $vuln_version_requested = $min_version && (
105             version->new( $version ) < version->new( $min_version )
106             );
107              
108 3 100 66     37 if ( $version == 0 && $vuln_version_requested ) {
    50          
109 2         6 $version = $min_version;
110             }
111             elsif ( $vuln_version_requested ) {
112 1         13 $self->log( "Prereq $module $version is vulnerable" );
113             }
114             }
115              
116 19         228 $str .= sprintf qq~%s%s "%s" => "%s";\n~,
117             $indent,
118             $type,
119             $module,
120             $version;
121             }
122              
123 17         58 $str .= $postfix;
124             }
125             }
126              
127 17         98866 return $str;
128             }
129              
130             sub _audit {
131 9     9   33 my ($audit, $module, $version) = @_;
132              
133 9         65 my $result = $audit->command( 'module', $module, $version );
134 9 50       239206 my ($module_data) = values %{ $result->{dists} || {} };
  9         54  
135 9 100       23 my @advisories = @{ $module_data->{advisories} || [] };
  9         44  
136              
137 9         26 my @versions;
138 9         29 for my $advisory ( @advisories ) {
139 17   100     119 my ($fixed_version) = ( $advisory->{fixed_versions} // '' ) =~ m{(v?[0-9]+(?:\.[0-9]+){0,2})};
140 17 100       47 next if !$fixed_version;
141              
142 7         81 my $version_object = version->new( $fixed_version );
143 7         27 push @versions, $version_object;
144             }
145              
146 9         38 my ($min_version) = sort { $b <=> $a } @versions;
  0         0  
147 9         83 return ( $min_version, scalar @advisories );
148             }
149              
150             __PACKAGE__->meta->make_immutable;
151              
152             1;
153              
154             __END__
155              
156             =pod
157              
158             =encoding utf-8
159              
160             =head1 NAME
161              
162             Dist::Zilla::Plugin::SyncCPANfile - Sync a cpanfile with the prereqs listed in dist.ini
163              
164             =head1 VERSION
165              
166             version 0.04
167              
168             =head1 SYNOPSIS
169              
170             # in dist.ini
171             [SyncCPANfile]
172              
173             # configure it yourself
174             [SyncCPANfile]
175             filename = my-cpanfile
176             comment = This is my cpanfile
177              
178             Unlike L<Dist::Zilla::Plugin::CPANFile> this plugin does not
179             add a I<cpanfile> to the distribution but to the "disk".
180              
181             =head1 CONFIG
182              
183             =head2 filename
184              
185             With this config you can change the filename for the file. It defaults
186             to I<cpanfile>.
187              
188             [SyncCPANfile]
189             filename = my-cpanfile
190              
191             =head2 comment
192              
193             The default comment says, that the I<cpanfile> was generated by this plugin.
194             You can define your own comment.
195              
196             [SyncCPANfile]
197             comment = This is my cpanfile
198             comment = line 2
199              
200             =head2 cpan_audit
201              
202             When I<cpan_audit> is enabled, the required module version is not defined (or 0),
203             and the module has vulnerabilities, the "fixed version" storied in L<CPAN::Audit>
204             is used as a minimum version.
205              
206             [SyncCPANfile]
207             cpan_audit = 1
208              
209             [Prereqs]
210             ExtUtils::MakeMaker = 0
211              
212             L<ExtUtils::MakeMaker> has a vulnerability in versions E<lt>= 7.21. As the minimum
213             version in the I<dist.ini> is 0 and I<cpan_audit> is enabled, the I<cpanfile>
214             will use 7.22 as the minimum version (as of June 2023).
215              
216             As this depends on the I<CPAN::Audit> database, you should update I<CPAN::Audit>
217             regularly.
218              
219             For dependencies where a minimum version is defined and the defined version is
220             vulnerable a warning is shown.
221              
222             =head1 SEE ALSO
223              
224             L<Dist::Zilla::Plugin::CPANFile>, L<Dist::Zilla::Plugin::GitHubREADME::Badge>
225              
226             =for Pod::Coverage after_build mvp_multivalue_args
227              
228             =head1 AUTHOR
229              
230             Renee Baecker <reneeb@cpan.org>
231              
232             =head1 COPYRIGHT AND LICENSE
233              
234             This software is Copyright (c) 2021 by Renee Baecker.
235              
236             This is free software, licensed under:
237              
238             The Artistic License 2.0 (GPL Compatible)
239              
240             =cut