File Coverage

blib/lib/App/makefilepl2cpanfile.pm
Criterion Covered Total %
statement 60 63 95.2
branch 17 22 77.2
condition 10 16 62.5
subroutine 7 7 100.0
pod 1 1 100.0
total 95 109 87.1


line stmt bran cond sub pod time code
1             package App::makefilepl2cpanfile;
2              
3 4     4   849052 use strict;
  4         9  
  4         165  
4 4     4   21 use warnings;
  4         8  
  4         246  
5              
6 4     4   653 use File::Slurp qw(read_file);
  4         58486  
  4         269  
7 4     4   2603 use YAML::Tiny;
  4         32618  
  4         310  
8 4     4   2366 use File::HomeDir;
  4         27646  
  4         5258  
9              
10             our $VERSION = '0.01';
11              
12             =head1 NAME
13              
14             App::makefilepl2cpanfile - Convert Makefile.PL to a cpanfile automatically
15              
16             =head1 SYNOPSIS
17              
18             use App::makefilepl2cpanfile;
19              
20             # Generate a cpanfile string
21             my $cpanfile_text = App::makefilepl2cpanfile::generate(
22             makefile => 'Makefile.PL',
23             existing => '', # optional, existing cpanfile content
24             with_develop => 1, # include developer dependencies
25             );
26              
27             # Write to disk
28             open my $fh, '>', 'cpanfile' or die $!;
29             print $fh $cpanfile_text;
30             close $fh;
31              
32             =head1 DESCRIPTION
33              
34             This module parses a `Makefile.PL` and produces a `cpanfile` with:
35              
36             =over 4
37              
38             =item * Runtime dependencies (`PREREQ_PM`)
39              
40             =item * Build, test, and configure requirements (`BUILD_REQUIRES`, `TEST_REQUIRES`, `CONFIGURE_REQUIRES`)
41              
42             =item * Optional author/development dependencies in a `develop` block
43              
44             =back
45              
46             The parsing is done **safely**, without evaluating the Makefile.PL.
47              
48             =head1 CONFIGURATION
49              
50             You may create a YAML file in:
51              
52             ~/.config/makefilepl2cpanfile.yml
53              
54             with a structure like:
55              
56             develop:
57             Perl::Critic: 0
58             Devel::Cover: 0
59             Test::Pod: 0
60             Test::Pod::Coverage: 0
61              
62             This will override the default development tools.
63              
64             =head1 METHODS
65              
66             =head2 generate(%args)
67              
68             Generates a cpanfile string.
69              
70             Arguments:
71              
72             =over 4
73              
74             =item * makefile
75              
76             Path to `Makefile.PL`. Defaults to `'Makefile.PL'`.
77              
78             =item * existing
79              
80             Optional string containing an existing cpanfile. Existing `develop` blocks are merged.
81              
82             =item * with_develop
83              
84             Boolean. Include default or configured author tools. Defaults to true if not overridden.
85              
86             =back
87              
88             Returns the cpanfile as a string.
89              
90             =cut
91              
92             # ----------------------------
93             # Main generate sub
94             # ----------------------------
95             sub generate {
96 4     4 1 920959 my (%args) = @_;
97              
98 4   50     30 my $makefile = $args{makefile} || 'Makefile.PL';
99 4   100     49 my $existing = $args{existing} || '';
100 4         13 my $with_dev = $args{with_develop};
101              
102 4         25 my %deps;
103             my $min_perl;
104              
105 4         31 my $content = read_file($makefile);
106              
107             # MIN_PERL_VERSION
108 4 100       617 if ($content =~ /MIN_PERL_VERSION\s*=>\s*['"]?([\d._]+)['"]?/) {
109 3         26 $min_perl = $1;
110             }
111              
112 4         44 my %map = (
113             PREREQ_PM => 'runtime',
114             BUILD_REQUIRES => 'build',
115             TEST_REQUIRES => 'test',
116             CONFIGURE_REQUIRES => 'configure',
117             );
118              
119             # Robust dependency hash extraction
120 4         18 for my $mf_key (keys %map) {
121 16         48 my $phase = $map{$mf_key};
122              
123 16         2993 while ($content =~ /
124             $mf_key \s*=>\s* \{
125             ( (?: [^{}] | \{[^}]*\} )*? )
126             \}
127             /gsx) {
128 8         41 my $block = $1;
129              
130 8         64 while ($block =~ /
131             ['"]([^'"]+)['"]
132             \s*=>\s*
133             ['"]?([\d._]+)?['"]?
134             /gx) {
135 21   100     200 $deps{$phase}{$1} = $2 // 0;
136             }
137             }
138             }
139              
140             # Preserve existing develop block
141 4 100       41 if ($existing =~ /on\s+'develop'\s*=>\s*sub\s*\{(.*?)\};/s) {
142 1         9 while ($1 =~ /requires\s+['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?/g) {
143 1   50     14 $deps{develop}{$1} //= $2 // 0;
      33        
144             }
145             }
146              
147             # Post-processing: develop block
148 4 100       16 if ($with_dev) {
149 2   100     15 $deps{develop} ||= {};
150              
151 2         14 my %default = (
152             'Perl::Critic' => 0,
153             'Devel::Cover' => 0,
154             'Test::Pod' => 0,
155             'Test::Pod::Coverage' => 0,
156             );
157              
158 2         25 my $cfg_file = File::HomeDir->my_home . '/.config/makefilepl2cpanfile.yml';
159 2 50       242 if (-r $cfg_file) {
160 0         0 my $y = YAML::Tiny->read($cfg_file)->[0];
161 0 0       0 %default = %{ $y->{develop} } if $y->{develop};
  0         0  
162             }
163              
164 2         14 for my $mod (keys %default) {
165 8   33     44 $deps{develop}{$mod} //= $default{$mod};
166             }
167             }
168              
169 4         24 return _emit(\%deps, $min_perl);
170             }
171              
172             # ----------------------------
173             # Emit cpanfile text
174             # ----------------------------
175             sub _emit {
176 4     4   20 my ($deps, $min_perl) = @_;
177              
178 4         12 my $out = "# Generated from Makefile.PL\n\n";
179 4 100       25 $out .= "requires 'perl', '$min_perl';\n\n" if $min_perl;
180              
181 4 50       19 if (my $rt = $deps->{runtime}) {
182 4         20 for my $m (sort keys %$rt) {
183 7         18 $out .= "requires '$m'";
184 7 100       29 $out .= ", '$rt->{$m}'" if $rt->{$m};
185 7         23 $out .= ";\n";
186             }
187 4         14 $out .= "\n";
188             }
189              
190 4         23 for my $phase (qw(configure build test develop)) {
191 16 100       52 my $h = $deps->{$phase} or next;
192 6 50       15 next unless %$h;
193              
194 6         21 $out .= "on '$phase' => sub {\n";
195 6         26 for my $m (sort keys %$h) {
196 23         42 $out .= " requires '$m'";
197 23 100       55 $out .= ", '$h->{$m}'" if $h->{$m};
198 23         41 $out .= ";\n";
199             }
200 6         14 $out .= "};\n";
201             }
202              
203 4         34 return $out;
204             }
205              
206             1;
207              
208             __END__