File Coverage

blib/lib/Dist/Mgr.pm
Criterion Covered Total %
statement 515 562 91.6
branch 178 242 73.5
condition 52 68 76.4
subroutine 77 92 83.7
pod 34 34 100.0
total 856 998 85.7


line stmt bran cond sub pod time code
1             package Dist::Mgr;
2              
3 25     25   2414256 use strict;
  25         37  
  25         771  
4 25     25   83 use warnings;
  25         36  
  25         899  
5 25     25   8182 use version;
  25         43200  
  25         119  
6              
7 25     25   9796 use Capture::Tiny qw(:all);
  25         452370  
  25         3604  
8 25     25   150 use Carp qw(croak cluck);
  25         30  
  25         1069  
9 25     25   9762 use CPAN::Uploader;
  25         1228166  
  25         955  
10 25     25   176 use Cwd qw(getcwd);
  25         34  
  25         1310  
11 25     25   1101 use Data::Dumper;
  25         13794  
  25         1005  
12 25     25   10020 use Digest::SHA;
  25         64870  
  25         1259  
13 25     25   11199 use Dist::Mgr::FileData qw(:all);
  25         95  
  25         4155  
14 25     25   9624 use Dist::Mgr::Git qw(:all);
  25         78  
  25         3163  
15 25     25   156 use File::Copy;
  25         31  
  25         1173  
16 25     25   115 use File::Copy::Recursive qw(rmove_glob);
  25         34  
  25         882  
17 25     25   104 use File::Path qw(make_path rmtree);
  25         40  
  25         875  
18 25     25   97 use File::Find::Rule;
  25         33  
  25         209  
19 25     25   14369 use JSON;
  25         206159  
  25         107  
20 25     25   2650 use Module::Starter;
  25         41  
  25         228  
21 25     25   2414 use PPI;
  25         35  
  25         385  
22 25     25   71 use Term::ReadKey;
  25         33  
  25         1241  
23 25     25   111 use Tie::File;
  25         30  
  25         408  
24              
25 25     25   67 use Exporter qw(import);
  25         34  
  25         3601  
26             our @ISA = qw(Exporter);
27             our @EXPORT_OK = qw(
28             add_bugtracker
29             add_repository
30             changes
31             changes_bump
32             changes_date
33             ci_badges
34             ci_github
35             config
36             config_file
37             copyright_info
38             copyright_bump
39             cpan_upload
40             git_add
41             git_commit
42             git_clone
43             git_pull
44             git_push
45             git_ignore
46             git_release
47             git_repo
48             git_status_differs
49             git_tag
50             init
51             make_dist
52             make_distclean
53             make_manifest
54             make_test
55             manifest_skip
56             manifest_t
57             move_distribution_files
58             remove_unwanted_files
59             version_bump
60             version_incr
61             version_info
62             );
63             our @EXPORT_PRIVATE = qw(
64             _dist_dir_re
65             _validate_git
66             );
67             our %EXPORT_TAGS = (
68             all => [@EXPORT_OK],
69             private => _export_private(),
70             );
71              
72             our $VERSION = '1.16';
73              
74             use constant {
75 25 50       147743 CONFIG_FILE => 'dist-mgr.json',
76             GITHUB_CI_FILE => 'github_ci_default.yml',
77             GITHUB_CI_PATH => '.github/workflows/',
78             CHANGES_FILE => 'Changes',
79             CHANGES_ORIG_SHA => '97624d56464d7254ef5577e4a0c8a098d6c6d9e6', # Module::Starter version
80             FSTYPE_IS_DIR => 1,
81             FSTYPE_IS_FILE => 2,
82             DEFAULT_DIR => 'lib/',
83             DEFAULT_POD_DIR => '.',
84             MAKE => $^O =~ /win32/i ? 'gmake' : 'make',
85 25     25   110 };
  25         28  
86              
87             # Public
88              
89             sub add_bugtracker {
90 6     6 1 55647 my ($author, $repo, $makefile) = @_;
91              
92 6 100 100     40 if (! defined $author || ! defined $repo) {
93 2         165 croak("Usage: add_bugtracker(\$author, \$repository_name)\n");
94             }
95              
96 4   100     21 $makefile //= 'Makefile.PL';
97              
98 4         15 _makefile_insert_bugtracker($author, $repo, $makefile);
99             }
100             sub add_repository {
101 6     6 1 230738 my ($author, $repo, $makefile) = @_;
102              
103 6 100 100     43 if (! defined $author || ! defined $repo) {
104 2         244 croak("Usage: add_repository(\$author, \$repository_name)\n");
105             }
106              
107 4   100     14 $makefile //= 'Makefile.PL';
108              
109 4         14 _makefile_insert_repository($author, $repo, $makefile);
110             }
111             sub changes {
112 2     2 1 11766 my ($module, $file) = @_;
113              
114 2 50       15 croak("changes() needs a module parameter") if ! defined $module;
115              
116 2   100     14 $file //= 'Changes';
117              
118             # Overwrite the Changes file if there aren't any dates in it
119              
120 2         6 my @contents;
121              
122 2         5 my $changes_date_count = 0;
123              
124 2 50       1517 if (-e $file) {
125 2         21 my ($contents, $tie) = _changes_tie($file);
126 2         17 $changes_date_count = grep /\d{4}-\d{2}-\d{2}/, $contents;
127 2         23 untie $tie;
128             }
129 2 50 33     127 if (! -e $file || ! $changes_date_count) {
130 2         34 my @contents = _changes_file($module);
131 2         9 _changes_write_file($file, \@contents);
132             }
133              
134 2         12 return @contents;
135             }
136             sub changes_bump {
137 1     1 1 3123 my ($version, $file) = @_;
138              
139 1 50       4 croak("changes_bump() requires a version sent in") if ! defined $version;
140 1         3 _validate_version($version);
141              
142 1   50     2 $file //= 'Changes';
143              
144 1         4 my ($contents, $tie) = _changes_tie($file);
145              
146 1         5 for (0..$#$contents) {
147 3 100       218 if ($contents->[$_] =~ /^\d+\.\d+\s+/) {
148 1         107 $contents->[$_-1] = "\n$version UNREL\n -\n\n";
149 1         1613 last;
150             }
151             }
152              
153 1         8 untie $tie;
154             }
155             sub changes_date {
156 1     1 1 2907 my ($file) = @_;
157              
158 1   50     5 $file //= 'Changes';
159              
160 1         4 my ($contents, $tie) = _changes_tie($file);
161              
162 1         15 my ($d, $m, $y) = (localtime)[3, 4, 5];
163 1         3 $y += 1900;
164 1         2 $m += 1;
165              
166 1 50       5 $m = "0$m" if length $m == 1;
167 1 50       3 $d = "0$d" if length $d == 1;
168              
169 1         3 for (0..$#$contents) {
170 3 100       254 if ($contents->[$_] =~ /^(.*)\s+UNREL/) {
171 1         77 $contents->[$_] = "$1 $y-$m-$d";
172 1         241 last;
173             }
174             }
175              
176 1         5 untie $tie;
177             }
178             sub ci_badges {
179 9 100   9 1 192906 if (scalar @_ < 2) {
180 2         233 croak("ci_badges() needs \$author and \$repo sent in");
181             }
182              
183 7         16 my ($author, $repo, $fs_entry) = @_;
184              
185 7   50     14 $fs_entry //= DEFAULT_DIR;
186              
187 7         15 my $exit = 0;
188              
189 7         22 for (_module_find_files($fs_entry)) {
190 7 100       4698 $exit = -1 if _module_insert_ci_badges($author, $repo, $_) == -1;
191             }
192              
193 7         5769 return $exit;
194             }
195             sub ci_github {
196 9     9 1 281443 my ($os) = @_;
197              
198 9 100 100     49 if (defined $os && ref $os ne 'ARRAY') {
199 3         303 croak("\$os parameter to ci_github() must be an array ref");
200             }
201              
202             # Add the CI file to MANIFEST.SKIP
203              
204 6 100       152 if (-e 'MANIFEST.SKIP') {
205 5 50       162 open my $fh, '<', 'MANIFEST.SKIP'
206             or croak("Can't open MANIFEST.SKIP for reading");
207              
208 5         168 my @makefile_skip_contents = <$fh>;
209              
210 5 50       40 if (grep !m|\.github$|, @makefile_skip_contents) {
211 5         44 close $fh;
212 5 50       126 open my $wfh, '>>', 'MANIFEST.SKIP'
213             or croak("Can't open MANIFEST.SKIP for writing");
214              
215 5         167 print $wfh '^\.github/';
216             }
217             }
218             else {
219 1 50       109 open my $wfh, '>>', 'MANIFEST.SKIP'
220             or croak("Can't open MANIFEST.SKIP for writing");
221              
222 1         60 print $wfh '^\.github/';
223             }
224              
225 6         36 my @contents = _ci_github_file($os);
226 6         25 _ci_github_write_file(\@contents);
227              
228 6         57 return @contents;
229             }
230             sub config {
231 21     21 1 181195 my ($args, $file) = @_;
232              
233 21 100       84 if (! defined $args) {
    100          
234 1         148 croak("config() requires \$args hash reference parameter");
235             }
236             elsif (ref $args ne 'HASH') {
237 1         89 croak("\$args parameter must be a hash reference.");
238             }
239              
240 19 100       63 $file = config_file() if ! defined $file;
241 19         24 my $conf;
242              
243 19 100 66     394 if (-e $file && -f $file) {
244             {
245 15         24 local $/;
  15         62  
246 15 50       1121 open my $fh, '<', $file or croak "Can't open config file $file: $!";
247 15         337 my $json = <$fh>;
248 15         99 $conf = decode_json $json;
249              
250 15         1608 for (keys %{ $conf }) {
  15         51  
251 31 100       241 delete $conf->{$_} if $conf->{$_} eq '';
252             }
253             }
254             }
255             else {
256             # No config file present
257 4         21 _config_file_write($file, _config_file());
258              
259 4         80 print "\nGenerated new configuration file: $file\n";
260             }
261              
262 19 100       71 %{ $args } = (%{ $args }, %{ $conf }) if $conf;
  15         36  
  15         29  
  15         26  
263              
264 19         59 return $args;
265             }
266             sub config_file {
267 20 50   20 1 365 my $file = $^O =~ /win32/i
268 0         0 ? "$ENV{USERPROFILE}/${\CONFIG_FILE}"
269 20         44 : "$ENV{HOME}/${\CONFIG_FILE}";
270              
271 20         49 return $file;
272             }
273             sub copyright_bump {
274 2     2 1 4864 my ($fs_entry) = @_;
275              
276 2   50     7 $fs_entry //= DEFAULT_POD_DIR;
277 2         1471 _validate_fs_entry($fs_entry);
278              
279 2         35 my ($year) = (localtime)[5];
280 2         5 $year += 1900;
281              
282 2         5 my @pod_files = _pod_find_files($fs_entry);
283 2         2064 my %info;
284              
285 2         4 for my $pod_file (@pod_files) {
286 9         273 my ($contents, $tie) = _pod_tie($pod_file);
287              
288 9         28 for (0 .. $#$contents) {
289             # Match a single year (Copyright 2016), a dash range
290             # (Copyright 2016-2019) or a comma range (Copyright 2016,2019)
291 144 100       10775 if ($contents->[$_] =~ /^(Copyright\s+)(\d{4})(?:\s*[-,]\s*(\d{4}))?(\s+.*)/) {
292 5         364 my ($prefix, $first, $second, $rest) = ($1, $2, $3, $4);
293              
294 5 100       24 if (defined $second) {
295             # A range: keep the first year, bump the latter to the
296             # current year, and normalize the separator to a dash
297 2         10 $contents->[$_] = "$prefix$first-$year$rest";
298             }
299             else {
300             # A single year: replace it with the current year
301 3         15 $contents->[$_] = "$prefix$year$rest";
302             }
303              
304 5         965 $info{$pod_file} = $year;
305 5         8 last;
306             }
307             }
308 9         295 untie $tie;
309             }
310              
311 2         83 return \%info;
312             }
313             sub copyright_info {
314 3     3 1 180840 my ($fs_entry) = @_;
315              
316 3   50     10 $fs_entry //= DEFAULT_POD_DIR;
317              
318 3         9 _validate_fs_entry($fs_entry);
319              
320 2         8 my @pod_files = _pod_find_files($fs_entry);
321              
322 2         2125 my %copyright_info;
323              
324 2         4 for my $file (@pod_files) {
325 9         15 my $copyright = _pod_extract_file_copyright($file);
326 9 100 66     35 next if ! defined $copyright || $copyright !~ /^\d{4}$/;
327 5 50       15 $copyright_info{$file} = $copyright if defined $copyright;
328             }
329              
330 2         9 return \%copyright_info;
331             }
332             sub cpan_upload {
333 5     5 1 170770 my ($dist_file_name, %args) = @_;
334              
335 5         11 config(\%args);
336              
337 5 100       25 if (! defined $dist_file_name) {
338 1         138 croak("cpan_upload() requires the name of a distribution file sent in");
339             }
340              
341 4 100       75 if (! -f $dist_file_name) {
342 1         122 croak("File name sent to cpan_upload() isn't a valid file");
343             }
344              
345 3   66     18 $args{user} //= $args{cpan_id};
346 3   66     9 $args{password} //= $args{cpan_pw};
347              
348 3 100       7 $args{user} = $ENV{CPAN_USERNAME} if ! $args{user};
349 3 100       7 $args{password} = $ENV{CPAN_PASSWORD} if ! $args{password};
350              
351 3 100 66     16 if (! $args{user} || ! $args{password}) {
352 2         212 croak("\ncpan_upload() requires --cpan_id and --cpan_pw");
353             }
354              
355 1 50       2 if ($args{dry_run}) {
356 1         23 print "\nCPAN upload is in dry run mode... nothing will be uploaded\n";
357             }
358              
359             CPAN::Uploader->upload_file(
360 1         11 $dist_file_name,
361             \%args
362             );
363              
364 1         217 print "\nSuccessfully uploaded $dist_file_name to the CPAN\n";
365              
366 1         11 return %args;
367             }
368             sub git_add {
369 0     0 1 0 _git_add();
370             }
371             sub git_ignore {
372 2     2 1 171634 my ($dir) = @_;
373              
374 2   100     13 $dir //= '.';
375              
376 2         12 my @content = _git_ignore_file();
377              
378 2         12 _git_ignore_write_file($dir, \@content);
379              
380 2         12 return @content;
381             }
382             sub git_commit {
383 0     0 1 0 _git_commit(@_);
384             }
385             sub git_clone {
386 0     0 1 0 _git_clone(@_);
387             }
388             sub git_push {
389 0     0 1 0 _git_push(@_);
390             }
391             sub git_pull {
392 0     0 1 0 _git_pull(@_);
393             }
394             sub git_release {
395 0     0 1 0 _git_release(@_);
396             }
397             sub git_repo {
398 0     0 1 0 _git_repo();
399             }
400             sub git_status_differs {
401 0     0 1 0 _git_status_differs(@_);
402             }
403             sub git_tag {
404 0     0 1 0 _git_tag(@_);
405             }
406             sub init {
407 8     8 1 349183 my (%args) = @_;
408              
409 8         30 config(\%args);
410              
411 8         55 my $cwd = getcwd();
412              
413 8 100       39 if ($cwd =~ _dist_dir_re()) {
414 1         142 croak "Can't run init() while in the '$cwd' directory";
415             }
416              
417 7 100       27 $args{license} = 'artistic2' if ! exists $args{license};
418 7         10 $args{builder} = 'ExtUtils::MakeMaker';
419              
420 7         13 for (qw(modules author email)) {
421 18 100       32 if (! exists $args{$_}) {
422 3         310 croak("init() requires '$_' in the parameter hash");
423             }
424             }
425              
426 4 100       15 if (ref $args{modules} ne 'ARRAY') {
427 1         133 croak("init()'s 'modules' parameter must be an array reference");
428             }
429              
430             # Module::Starter 1.79+ added multi-author support and now requires
431             # 'author' to be an arrayref of 'Name ' strings; older versions
432             # expect a plain scalar. Normalize for the installed version while
433             # leaving $args{author} untouched for _module_write_template() below
434 3         12 my %distro_args = %args;
435              
436 3 50       20 if ($Module::Starter::VERSION >= 1.79) {
437 3         12 $distro_args{author} = ["$args{author} <$args{email}>"];
438             }
439              
440 3 100       7 if ($args{verbose}) {
441 2         3 delete $distro_args{verbose};
442 2         30 Module::Starter->create_distro(%distro_args);
443             }
444             else {
445             capture_merged {
446 1     1   1362 Module::Starter->create_distro(%distro_args);
447 1         25 };
448             }
449              
450 3         139438 my ($module) = (@{ $args{modules} })[0];
  3         37  
451 3         16 my $module_file = $module;
452 3         34 $module_file =~ s/::/\//g;
453 3         14 $module_file = "lib/$module_file.pm";
454              
455 3         13 my $module_dir = $module;
456 3         14 $module_dir =~ s/::/-/g;
457              
458 3 50       43 chdir $module_dir or croak("Can't change into directory '$module_dir'");
459              
460 3 50       272 unlink $module_file
461             or croak("Can't delete the Module::Starter module '$module_file': $!");
462              
463 3         55 _module_write_template($module_file, $module, $args{author}, $args{email});
464              
465 3 50       96 chdir '..' or croak "Can't change into original directory";
466             }
467             sub manifest_skip {
468 2     2 1 171474 my ($dir) = @_;
469              
470 2   100     19 $dir //= '.';
471              
472 2         19 my @content = _manifest_skip_file();
473              
474 2         13 _manifest_skip_write_file($dir, \@content);
475              
476 2         15 return @content;
477             }
478             sub manifest_t {
479 2     2 1 175654 my ($dir) = @_;
480              
481 2   100     25 $dir //= './t';
482              
483 2         12 my @content = _manifest_t_file();
484              
485 2         12 _manifest_t_write_file($dir, \@content);
486              
487 2         14 return @content;
488             }
489             sub move_distribution_files {
490 4     4 1 3264 my ($module) = @_;
491              
492 4 100       19 if (! defined $module) {
493 1         150 croak("_move_distribution_files() requires a module name sent in");
494             }
495              
496 3         9 my $module_dir = $module;
497 3         10 $module_dir =~ s/::/-/g;
498              
499 3 100       46 my @move_count = rmove_glob("$module_dir/*", '.')
500             or croak("Can't move files from the '$module_dir' directory: $!");
501              
502 2         64355 my $dist_count = _default_distribution_file_count($module);
503              
504 2         54 for my $outer_idx (0..$#move_count) {
505 16         19 my $outer = $move_count[$outer_idx];
506 16         25 for my $inner_idx (0..$#$outer) {
507 16         25 my $inner = $move_count[$outer_idx][$inner_idx];
508 16         22 for (0..$#$inner) {
509 48 100       85 if ($inner->[$_] != $dist_count->[$outer_idx][$inner_idx][$_]) {
510 1         324 croak("Results from the move are mismatched... bailing out");
511             }
512             }
513             }
514             }
515              
516 1 50       362 rmtree $module_dir or croak("Couldn't remove the '$module_dir' directory");
517              
518 1         28 return 0;
519             }
520             sub remove_unwanted_files {
521 2     2 1 55759 for (_unwanted_filesystem_entries()) {
522 8         1521 rmtree $_;
523             }
524 2         26 make_manifest();
525 2         129 return 0;
526             }
527             sub make_dist {
528 0     0 1 0 my ($verbose) = @_;
529              
530 0         0 my $cmd = "${\MAKE} dist";
  0         0  
531 0 0   0   0 $verbose ? `$cmd` : capture_merged {`$cmd`};
  0         0  
532              
533 0 0       0 if ($? != 0) {
534 0         0 croak("Exit code $? returned... '${\MAKE} dist' failed");
  0         0  
535             }
536              
537 0         0 return $?;
538             }
539             sub make_distclean {
540 3     3 1 34 my ($verbose) = @_;
541              
542 3         21 my $cmd = "${\MAKE} distclean";
  3         28  
543 3 50   3   569 $verbose ? print `$cmd` : capture_merged {`$cmd`};
  3         945186  
544              
545 3 50       3637 if ($? != 0) {
546 0         0 croak("Exit code $? returned... '${\MAKE} distclean' failed\n");
  0         0  
547             }
548              
549 3         82 return $?;
550             }
551             sub make_manifest {
552 3     3 1 1235 my ($verbose) = @_;
553              
554 3 50       22 if ($verbose) {
555 0 0       0 if (-f 'MANIFEST') {
556 0 0       0 unlink 'MANIFEST' or die "make_manifest() Couldn't remove MANIFEST\n";
557             }
558 0         0 print `$^X Makefile.PL`;
559 0         0 print `${\MAKE} manifest`;
  0         0  
560 0         0 make_distclean($verbose);
561             }
562             else {
563             capture_merged {
564 3 100   3   4449 if (-f 'MANIFEST') {
565 1 50       89 unlink 'MANIFEST' or die "make_manifest() Couldn't remove MANIFEST\n";
566             }
567 3         2604673 `$^X Makefile.PL`;
568 3         129 `${\MAKE} manifest`;
  3         1086078  
569 3         154 make_distclean($verbose);
570 3         125 };
571             }
572              
573 3 50       2127 if ($? != 0) {
574 0         0 croak("Exit code $? returned... '${\MAKE} manifest' failed\n");
  0         0  
575             }
576              
577 3         17 return $?;
578             }
579             sub make_test {
580 0     0 1 0 my ($verbose) = @_;
581              
582 0 0       0 if ($verbose) {
583 0         0 print `$^X Makefile.PL`;
584 0         0 print `${\MAKE} test`;
  0         0  
585             }
586             capture_merged {
587 0     0   0 `$^X Makefile.PL`;
588 0         0 `${\MAKE} test`;
  0         0  
589 0         0 };
590              
591 0 0       0 if ($? != 0) {
592 0         0 croak("Exit code $? returned... '${\MAKE} test' failed\n");
  0         0  
593             }
594              
595 0         0 return $?;
596             }
597             sub version_bump {
598 14     14 1 386730 my ($version, $fs_entry) = @_;
599              
600 14         38 my $dry_run = 0;
601              
602 14 100 100     106 if (defined $version && $version =~ /^-/) {
603 5         50 print "\nDry run\n\n";
604 5         21 $version =~ s/-//;
605 5         8 $dry_run = 1;
606             }
607              
608 14   100     46 $fs_entry //= DEFAULT_DIR;
609              
610 14         51 _validate_version($version);
611 10         31 _validate_fs_entry($fs_entry);
612              
613 8         27 my @module_files = _module_find_files($fs_entry);
614              
615 8         6959 my %files;
616              
617 8         18 for (@module_files) {
618 23         53 my $current_version = _module_extract_file_version($_);
619 23         66 my $version_line = _module_extract_file_version_line($_);
620 23         6204 my @file_contents = _module_fetch_file_contents($_);
621              
622 23 100       50 if (! defined $version_line) {
623 3         8 next;
624             }
625              
626 20 100       41 if (! defined $current_version) {
627 3         11 next;
628             }
629              
630 17 100       252 if (version->parse($current_version) >= version->parse($version)) {
631 1         191 croak(
632             "Your new version $version must be greater than the current " .
633             "one, $current_version"
634             );
635             }
636              
637 16         40 my $mem_file;
638              
639 16 50       127 open my $wfh, '>', \$mem_file or croak("Can't open mem file!: $!");
640              
641 16         33 for my $line (@file_contents) {
642 495         450 chomp $line;
643              
644 495 100       583 if ($line eq $version_line) {
645 16         382 $line =~ s/$current_version/$version/;
646             }
647              
648 495         434 $line .= "\n";
649              
650             # Write out the line to the in-memory temp file
651 495         489 print $wfh $line;
652              
653 495         548 $files{$_}{from} = $current_version;
654 495         532 $files{$_}{to} = $version;
655             }
656              
657 16         32 close $wfh;
658              
659 16         36 $files{$_}{dry_run} = $dry_run;
660 16         33 $files{$_}{content} = $mem_file;
661              
662 16 100       66 if (! $dry_run) {
663             # Write out the actual file
664 5         14 _module_write_file($_, $mem_file);
665             }
666             }
667 7         36 return \%files;
668             }
669             sub version_incr {
670 1006     1006 1 821683 my ($version) = @_;
671              
672 1006 100       2201 croak("version_incr() needs a version number sent in") if ! defined $version;
673              
674 1005         2883 _validate_version($version);
675              
676             # Increment the least-significant digit while preserving the version's
677             # precision (eg. 3.1802 -> 3.1803, not 3.19)
678              
679 1004 50       6738 my $decimals = $version =~ /\.(\d+)$/ ? length $1 : 0;
680 1004         2044 my $increment = 1 / 10 ** $decimals;
681              
682 1004         10129 return sprintf("%.${decimals}f", $version + $increment);
683             }
684             sub version_info {
685 5     5 1 5841 my ($fs_entry) = @_;
686              
687 5   50     15 $fs_entry //= DEFAULT_DIR;
688              
689 5         28 _validate_fs_entry($fs_entry);
690              
691 5         20 my @module_files = _module_find_files($fs_entry);
692              
693 5         5375 my %version_info;
694              
695 5         12 for (@module_files) {
696 15         32 my $version = _module_extract_file_version($_);
697 15         42 $version_info{$_} = $version;
698             }
699              
700 5         24 return \%version_info;
701             }
702              
703             # Changes file related
704              
705             sub _changes_tie {
706             # Ties the Changes file to an array
707              
708 4     4   11 my ($changes) = @_;
709 4 50       11 croak("_changes_tie() needs a Changes file name sent in") if ! defined $changes;
710              
711 4         336 my $tie = tie my @changes, 'Tie::File', $changes;
712 4         634 return (\@changes, $tie);
713             }
714             sub _changes_write_file {
715             # Writes out the custom Changes file
716              
717 2     2   27 my ($file, $content) = @_;
718              
719 2 50       180 open my $fh, '>', $file or cluck("Can't open file $file: $!");
720              
721 2         11 for (@$content) {
722 8         33 print $fh "$_\n"
723             }
724              
725 2         188 close $fh;
726              
727 2         17 return 0;
728             }
729              
730             # CI related
731              
732             sub _ci_github_write_file {
733             # Writes out the Github Actions config file
734              
735 7     7   290 my ($contents) = @_;
736              
737 7 100       22 if (ref $contents ne 'ARRAY') {
738 1         78 croak("_ci_github_write_file() requires an array ref of contents");
739             }
740              
741 6   50     29 my $ci_file //= GITHUB_CI_PATH . GITHUB_CI_FILE;
742              
743 6 100       802 make_path(GITHUB_CI_PATH) if ! -d GITHUB_CI_PATH;
744              
745 6 50       505 open my $fh, '>', $ci_file or croak $!;
746              
747 6         378 print $fh "$_\n" for @$contents;
748             }
749              
750             # Configuration related
751              
752             sub _config_file_write {
753 4     4   9 my ($file, $contents) = @_;
754              
755 4 50       13 if (ref $contents ne 'HASH') {
756 0         0 croak("_config_file_write() requires a hash ref of contents");
757             }
758              
759 4         43 my $jobj = JSON->new;
760              
761 4         38 my $json = $jobj->pretty->encode($contents);
762              
763 4 50       2218 open my $fh, '>', $file or croak "Can't open config $file for writing: $!";
764              
765 4         256 print $fh $json;
766              
767             }
768              
769             # Distribution related
770              
771             sub _default_distribution_file_count {
772             # Returns the file count in a distribution
773             # This is used to ensure everything moved OK
774              
775 1     1   9 my ($module) = @_;
776              
777             # The lib/ tree depth grows with the module's namespace depth
778             # (eg. Foo::Bar::Baz => lib/Foo/Bar/Baz.pm), so derive that entry's
779             # file/dir counts from the number of name components. For a module
780             # with N components, moving lib/ reports [N + 1, N, 0]. Accept either
781             # the '::' module form or the '-' distribution form for the count.
782              
783 1 50       59 my @parts = defined $module ? split /::|-/, $module : ('Foo', 'Bar');
784 1         5 my $depth = scalar @parts;
785              
786             return [
787 1         14 [ [1, 0, 0] ],
788             [ [1, 0, 0] ],
789             [ [$depth + 1, $depth, 0] ],
790             [ [1, 0, 0] ],
791             [ [1, 0, 0] ],
792             [ [1, 0, 0] ],
793             [ [5, 1, 0] ],
794             [ [2, 1, 0] ],
795             ];
796             }
797              
798             # Git related
799              
800             sub _git_ignore_write_file {
801             # Writes out the .gitignore file
802              
803 2     2   5 my ($dir, $content) = @_;
804              
805 2 50       264 open my $fh, '>', "$dir/.gitignore" or croak $!;
806              
807 2         8 for (@$content) {
808 48         64 print $fh "$_\n"
809             }
810              
811 2         81 return 0;
812             }
813              
814             # Makefile related
815              
816             sub _makefile_tie {
817             # Ties the Makefile.PL file to an array
818              
819 8     8   15 my ($mf) = @_;
820 8 50       17 croak("_makefile_tie() needs a Makefile name sent in") if ! defined $mf;
821              
822 8         76 my $tie = tie my @mf, 'Tie::File', $mf;
823 8         1240 return (\@mf, $tie);
824             }
825             sub _makefile_insert_meta_merge {
826             # Inserts the META_MERGE section into Makefile.PL
827              
828 6     6   19 my ($mf) = @_;
829              
830 6 50       17 croak("_makefile_insert_meta_merge() needs a Makefile tie sent in") if ! defined $mf;
831              
832             # Check to ensure we're not duplicating
833 6 100       16 return if grep /META_MERGE/, @$mf;
834              
835 4         12005 for (0..$#$mf) {
836 51 100       4060 if ($mf->[$_] =~ /MIN_PERL_VERSION/) {
837 4         410 splice @$mf, $_+1, 0, _makefile_section_meta_merge();
838 4         1837 last;
839             }
840             }
841             }
842             sub _makefile_insert_bugtracker {
843             # Inserts bugtracker information into Makefile.PL
844              
845 5     5   13 my ($author, $repo, $makefile) = @_;
846              
847 5 100       33 if (! defined $makefile) {
848 1         105 croak("_makefile_insert_bugtracker() needs author, repo and makefile");
849             }
850              
851 4         13 my ($mf, $tie) = _makefile_tie($makefile);
852              
853 4 100       19 return -1 if grep /bugtracker/, @$mf;
854              
855 3 50       7588 if (grep ! /META_MERGE/, @$mf) {
856 3         9244 _makefile_insert_meta_merge($mf);
857             }
858              
859 3         3195 for (0..$#$mf) {
860 47 100       2556 if ($mf->[$_] =~ /resources => \{/) {
861 3         263 splice @$mf, $_+1, 0, _makefile_section_bugtracker($author, $repo);
862 3         1242 last;
863             }
864             }
865 3         8 untie $tie;
866              
867 3         12 return 0;
868             }
869             sub _makefile_insert_repository {
870             # Inserts repository information to Makefile.PL
871              
872 5     5   12 my ($author, $repo, $makefile) = @_;
873              
874 5 100       12 if (! defined $makefile) {
875 1         97 croak("_makefile_insert_repository() needs author, repo and makefile");
876             }
877              
878 4         13 my ($mf, $tie) = _makefile_tie($makefile);
879              
880 4 100       22 return -1 if grep /repository/, @$mf;
881              
882 3 50       7932 if (grep ! /META_MERGE/, @$mf) {
883 3         9150 _makefile_insert_meta_merge($mf);
884             }
885              
886 3         5074 for (0..$#$mf) {
887 47 100       2785 if ($mf->[$_] =~ /resources => \{/) {
888 3         234 splice @$mf, $_+1, 0, _makefile_section_repo($author, $repo);
889 3         1204 last;
890             }
891             }
892 3         7 untie $tie;
893              
894 3         14 return 0;
895             }
896              
897             # MANIFEST related
898              
899             sub _manifest_skip_write_file {
900             # Writes out the MANIFEST.SKIP file
901              
902 2     2   5 my ($dir, $content) = @_;
903              
904 2 50       277 open my $fh, '>', "$dir/MANIFEST.SKIP" or croak $!;
905              
906 2         10 for (@$content) {
907 74         112 print $fh "$_\n"
908             }
909              
910 2         101 return 0;
911             }
912             sub _manifest_t_write_file {
913             # Writes out the t/manifest.t test file
914              
915 2     2   4 my ($dir, $content) = @_;
916              
917 2 50       447 open my $fh, '>', "$dir/manifest.t"
918             or croak("Can't open t/manifest.t for writing: $!\n");
919              
920 2         10 for (@$content) {
921 28         67 print $fh "$_\n"
922             }
923              
924 2         170 return 0;
925             }
926              
927             # Module related
928              
929             sub _module_extract_file_version {
930             # Extracts the version number from a module's $VERSION definition line
931              
932 38     38   67 my ($module_file) = @_;
933              
934 38         64 my $version_line = _module_extract_file_version_line($module_file);
935              
936 38 100       10333 if (defined $version_line) {
937              
938 33 50       179 if ($version_line =~ /=(.*)$/) {
939 33         85 my $ver = $1;
940              
941 33         142 $ver =~ s/\s+//g;
942 33         70 $ver =~ s/;//g;
943 33         63 $ver =~ s/[a-zA-Z]+//g;
944 33         43 $ver =~ s/"//g;
945 33         70 $ver =~ s/'//g;
946              
947 33 100       47 if (! defined eval { version->parse($ver); 1 }) {
  33         297  
  28         105  
948 5         43 warn("$_: Can't find a valid version\n");
949 5         34 return undef;
950             }
951              
952 28         86 return $ver;
953             }
954             }
955             else {
956 5         63 warn("$_: Can't find a \$VERSION definition\n");
957             }
958 5         54 return undef;
959             }
960             sub _module_extract_file_version_line {
961             # Extracts the $VERSION definition line from a module file
962              
963 61     61   78 my ($module_file) = @_;
964              
965 61         311 my $doc = PPI::Document->new($module_file);
966              
967             my $token = $doc->find(
968             sub {
969 3262 100   3262   25025 $_[1]->isa("PPI::Statement::Variable")
970             and $_[1]->content =~ /\$VERSION/;
971             }
972 61         341052 );
973              
974 61 100       695 return undef if ref $token ne 'ARRAY';
975              
976 53         97 my $version_line = $token->[0]->content;
977              
978 53         1201 return $version_line;
979             }
980             sub _module_fetch_file_contents {
981             # Fetches the file contents of a module file
982              
983 23     23   39 my ($file) = @_;
984              
985 23 50       1031 open my $fh, '<', $file
986             or croak("Can't open file '$file' for reading!: $!");
987              
988 23         567 my @contents = <$fh>;
989 23         221 close $fh;
990 23         216 return @contents;
991             }
992             sub _module_find_files {
993             # Finds module files
994              
995 21     21   610 my ($fs_entry, $module) = @_;
996              
997 21   50     45 $fs_entry //= DEFAULT_DIR;
998              
999 21 100       47 if (defined $module) {
1000 1         3 $module =~ s/::/\//g;
1001 1         20 $module .= '.pm';
1002             }
1003             else {
1004 20         38 $module = '*.pm';
1005             }
1006              
1007              
1008 21         705 return File::Find::Rule->file()
1009             ->name($module)
1010             ->in($fs_entry);
1011             }
1012             sub _module_insert_ci_badges {
1013             # Inserts the CI and Coveralls badges into POD
1014              
1015 7     7   16 my ($author, $repo, $module_file) = @_;
1016              
1017 7         15 my ($mf, $tie) = _module_tie($module_file);
1018              
1019 7 100       51 return -1 if grep /badge\.svg/, @$mf;
1020              
1021 5         10555 for (0..$#$mf) {
1022 68 100       5312 if ($mf->[$_] =~ /^=head1 NAME/) {
1023 3         273 splice @$mf, $_+3, 0, _module_section_ci_badges($author, $repo);
1024 3         2948 last;
1025             }
1026             }
1027 5         143 untie $tie;
1028              
1029 5         20 return 0;
1030             }
1031             sub _module_tie {
1032             # Ties a module file to an array
1033              
1034 7     7   11 my ($mod_file) = @_;
1035 7 50       16 croak("Acme-STEVEB() needs a module file name sent in") if ! defined $mod_file;
1036              
1037 7         50 my $tie = tie my @mf, 'Tie::File', $mod_file;
1038 7         887 return (\@mf, $tie);
1039             }
1040             sub _module_write_file {
1041             # Writes out a Perl module file
1042              
1043 5     5   15 my ($module_file, $content) = @_;
1044              
1045 5 50       477 open my $wfh, '>', $module_file or croak("Can't open '$module_file' for writing!: $!");
1046              
1047 5         34 print $wfh $content;
1048              
1049 5 50       406 close $wfh or croak("Can't close the temporary memory module file!: $!");
1050             }
1051             sub _module_write_template {
1052             # Writes out our custom module template after init()
1053              
1054 7     7   32 my ($module_file, $module, $author, $email) = @_;
1055              
1056 7 100       31 if (! defined $module_file) {
1057 1         87 croak("_module_write_template() needs the module's file name sent in");
1058             }
1059              
1060 6 100 100     67 if (! defined $module || ! defined $author || ! defined $email) {
      100        
1061 3         331 croak("_module_template_file() requires 'module', 'author' and 'email' parameters");
1062             }
1063              
1064 3         54 my @content = _module_template_file($module, $author, $email);
1065              
1066 3 50       417 open my $wfh, '>', $module_file or croak("Can't open '$module_file' for writing!: $!");
1067              
1068 3         199 print $wfh "$_\n" for @content;
1069             }
1070              
1071             # POD related
1072              
1073             sub _pod_extract_file_copyright {
1074             # Extracts the copyright year from POD
1075              
1076 9     9   26 my ($module_file) = @_;
1077              
1078 9         23 my $copyright_line = _pod_extract_file_copyright_line($module_file);
1079              
1080 9 50       36 if (defined $copyright_line) {
1081 9 100       23 if ($copyright_line =~ /^Copyright\s+(\d{4})(?:\s*[-,]\s*(\d{4}))?\s+\w+/) {
1082             # For a range, report the latter (most recent) year
1083 5 100       18 return defined $2 ? $2 : $1;
1084             }
1085             }
1086             else {
1087 0         0 warn("$_: Can't find a Copyright definition\n");
1088             }
1089 4         7 return undef;
1090             }
1091             sub _pod_extract_file_copyright_line {
1092             # Extracts the Copyright line from a module file
1093              
1094 9     9   12 my ($pod_file) = @_;
1095              
1096 9 50       236 open my $fh, '<', $pod_file or croak("Can't open POD file $pod_file: $!");
1097              
1098 9         139 while (<$fh>) {
1099 144 100       289 if (/^Copyright\s+\d{4}(?:\s*[-,]\s*\d{4})?\s+\w+/) {
1100 5         59 return $_;
1101             }
1102             }
1103             }
1104             sub _pod_find_files {
1105             # Finds POD files
1106              
1107 4     4   8 my ($fs_entry) = @_;
1108              
1109 4   50     7 $fs_entry //= DEFAULT_POD_DIR;
1110              
1111 4         135 return File::Find::Rule->file()
1112             ->name('*.pod', '*.pm', '*.pl')
1113             ->in($fs_entry);
1114             }
1115             sub _pod_tie {
1116             # Ties a POD file to an array
1117              
1118 9     9   12 my ($pod_file) = @_;
1119 9 50       19 croak("_pod_tie() needs a POD file name sent in") if ! defined $pod_file;
1120              
1121 9         36 my $tie = tie my @pf, 'Tie::File', $pod_file;
1122 9         1034 return (\@pf, $tie);
1123             }
1124              
1125             # Validation related
1126              
1127             sub _dist_dir_re {
1128             # Capture permutations of the distribution directory for various
1129             # CPAN testers
1130             # Use YAPE::Regex::Explain for details
1131              
1132 15     15   701252 return qr/dist-mgr(?:-\d+\.\d+)?(?:-\w+|_\d+)?$/i;
1133             }
1134             sub _validate_git {
1135 0 0   0   0 my $sep = $^O =~ /win32/i ? ';' : ':';
1136 0         0 return grep {-x "$_/git" } split /$sep/, $ENV{PATH};
  0         0  
1137             }
1138             sub _validate_fs_entry {
1139             # Validates a file system entry as valid
1140              
1141 20     20   35 my ($fs_entry) = @_;
1142              
1143 20 50       51 cluck("Need name of dir or file!") if ! defined $fs_entry;
1144              
1145 20 100       428 return FSTYPE_IS_DIR if -d $fs_entry;
1146 9 100       77 return FSTYPE_IS_FILE if -f $fs_entry;
1147              
1148 3         459 croak("File system entry '$fs_entry' is invalid");
1149             }
1150             sub _validate_version {
1151             # Parses a version number to ensure it is valid
1152              
1153 1020     1020   1355 my ($version) = @_;
1154              
1155 1020 100       2056 croak("version parameter must be supplied!") if ! defined $version;
1156              
1157 1018 100       1296 if (! defined eval { version->parse($version); 1 }) {
  1018         7010  
  1015         3128  
1158 3         311 croak("The version number '$version' specified is invalid");
1159             }
1160             }
1161              
1162             # Miscellaneous
1163              
1164             sub _export_private {
1165 25     25   66 push @EXPORT_OK, @EXPORT_PRIVATE;
1166 25         92 return \@EXPORT_OK;
1167             }
1168       0     sub __placeholder {}
1169              
1170             1;
1171             __END__