App-prepare4release
view release on metacpan or search on metacpan
lib/App/prepare4release.pm view on Meta::CPAN
my $min = $class->resolve_min_perl_for_badge(
$app->{config} // {}, $mf_content, $vf );
$min = 'v5.10.0' unless defined $min && length $min;
$inner = $class->build_pod_badge_markdown(
$cwd, $app, $opts, $opts->{cpan} ? 1 : 0,
$mf_snippets, $identity, $min
);
}
open my $fh, '<:encoding(UTF-8)', $readme
or croak "Cannot open README.md '$readme': $!";
local $/;
my $text = <$fh> // '';
close $fh;
$text = $class->_strip_readme_badge_markdown_block($text);
# Trailing "\n\n" = mandatory blank line after the last badge line.
my $block = $inner . "\n\n";
my $new_text = $class->_insert_readme_badges_after_regen( $text, $block );
return if $new_text eq $text;
open my $out, '>:encoding(UTF-8)', $readme
or croak "Cannot write README.md '$readme': $!";
print {$out} $new_text;
close $out;
warn "[prepare4release] updated README badges in $readme\n" if $verbose;
return;
}
sub strip_pod_badges_from_version_from {
my ( $class, $vf, $verbose ) = @_;
return unless $vf && -f $vf;
my ( $code, $pod ) = $class->split_pm_code_and_pod($vf);
return unless length $pod;
return unless $pod =~ /PREPARE4RELEASE_BADGES/;
my $new_pod = $pod;
$new_pod =~ s{
(?:^=begin\s+html\s*\R)?
\s*<!--\s*PREPARE4RELEASE_BADGES\s*-->\s*
.*?
<!--\s*/PREPARE4RELEASE_BADGES\s*-->\s*
(?:\R=end\s+html\s*)?
}{}msx;
return if $new_pod eq $pod;
my $out = $code . "\n__END__\n" . $new_pod;
open my $out_fh, '>:encoding(UTF-8)', $vf
or croak "Cannot write '$vf': $!";
print {$out_fh} $out;
close $out_fh;
warn "[prepare4release] removed legacy POD badge block from $vf\n" if $verbose;
return;
}
sub list_files_for_eol_xt {
my ( $class, $cwd ) = @_;
my @out;
for my $f (qw(Makefile.PL Build.PL cpanfile prepare4release.json)) {
my $p = File::Spec->catfile( $cwd, $f );
push @out, $f if -f $p;
}
push @out, map { File::Spec->abs2rel( $_, $cwd ) }
$class->find_lib_pm_files($cwd);
my $bin = File::Spec->catfile( $cwd, 'bin' );
if ( -d $bin ) {
opendir my $dh, $bin or croak "opendir bin: $!";
while ( my $e = readdir $dh ) {
next if $e =~ /^\./;
my $rel = File::Spec->catfile( 'bin', $e );
push @out, $rel if -f File::Spec->catfile( $cwd, $rel );
}
closedir $dh;
}
for my $td (qw(t xt)) {
my $root = File::Spec->catfile( $cwd, $td );
next unless -d $root;
File::Find::find(
{
no_chdir => 1,
wanted => sub {
return unless -f;
return unless $File::Find::name =~ /\.(t|pm|pl)\z/;
push @out, File::Spec->abs2rel( $File::Find::name, $cwd );
},
},
$root
);
}
my %seen;
@out = grep { !$seen{$_}++ } sort @out;
return @out;
}
sub ensure_xt_author_tests {
my ( $class, $cwd, $verbose ) = @_;
my $xtd = File::Spec->catfile( $cwd, 'xt', 'author' );
make_path($xtd);
my $pod_xt = File::Spec->catfile( $xtd, 'pod.t' );
if ( !-e $pod_xt ) {
my $body = <<'XT';
#!perl
use strict;
use warnings;
use Test2::V1;
use Test2::Tools::Basic qw(skip_all);
BEGIN {
eval {
require Test::Pod;
Test::Pod->import;
1;
} or skip_all 'Test::Pod is required for author tests';
}
all_pod_files_ok();
XT
$class->_write_if_absent( $pod_xt, $body, $verbose );
}
my $pc_xt = File::Spec->catfile( $xtd, 'pod-coverage.t' );
if ( !-e $pc_xt ) {
my $body = <<'XT';
#!perl
use strict;
use warnings;
use Test2::V1;
use Test2::Tools::Basic qw(skip_all);
BEGIN {
eval {
require Test::Pod::Coverage;
Test::Pod::Coverage->import;
1;
} or skip_all 'Test::Pod::Coverage is required for author tests';
}
all_pod_coverage_ok();
XT
$class->_write_if_absent( $pc_xt, $body, $verbose );
}
my @eol = $class->list_files_for_eol_xt($cwd);
my $eol_xt = File::Spec->catfile( $xtd, 'eol.t' );
if ( !-e $eol_xt ) {
my $list = join "\n", map { ' ' . $_ } @eol;
my $head = <<'EOL_HEAD';
#!perl
use strict;
use warnings;
use Test2::V1;
use Test2::Tools::Basic qw(skip_all done_testing);
BEGIN {
eval {
require Test::EOL;
Test::EOL->import;
1;
} or skip_all 'Test::EOL is required for author tests';
}
my @files = qw(
EOL_HEAD
my $tail = <<'EOL_TAIL';
);
eol_unix_ok($_) for @files;
done_testing;
EOL_TAIL
my $body = $head . $list . $tail;
$class->_write_if_absent( $eol_xt, $body, $verbose );
}
return;
}
sub _write_if_absent {
my ( $class, $path, $body, $verbose ) = @_;
open my $fh, '>:encoding(UTF-8)', $path
or croak "Cannot write '$path': $!";
print {$fh} $body;
close $fh;
warn "[prepare4release] wrote $path\n" if $verbose;
return;
}
sub _collect_t_files {
my ( $class, $cwd ) = @_;
my @out;
for my $root_name (qw(t xt)) {
my $root = File::Spec->catfile( $cwd, $root_name );
next unless -d $root;
File::Find::find(
{
no_chdir => 1,
wanted => sub {
return unless -f && /\.t\z/;
push @out, File::Spec->abs2rel( $File::Find::name, $cwd );
},
},
$root
);
}
my %seen;
@out = grep { !$seen{$_}++ } sort @out;
return @out;
}
sub file_uses_legacy_assertion_framework {
my ( $class, $path ) = @_;
open my $fh, '<:encoding(UTF-8)', $path
or return 0;
while ( my $line = <$fh> ) {
next if $line =~ /^\s*#/;
next if $line =~ /^\s*=/;
return 1 if $line =~ /^\s*use\s+Test::More\b/;
return 1 if $line =~ /^\s*use\s+Test::Most\b/;
}
close $fh;
return 0;
}
sub warn_legacy_test_frameworks {
my ( $class, $cwd ) = @_;
my %legacy_ok = map { $_ => 1 } qw(
xt/author/cpants.t
xt/author/pause-permissions.t
xt/author/pod-coverage.t
xt/author/version.t
);
my @bad;
lib/App/prepare4release.pm view on Meta::CPAN
use App::prepare4release;
App::prepare4release->run(@ARGV);
=head1 DESCRIPTION
Run from the distribution root (where F<prepare4release.json> and F<Makefile.PL>
live). The tool:
=over 4
=item *
Loads F<prepare4release.json> and resolves C<module_name> / C<version> / C<dist_name>
when omitted (from F<Makefile.PL> and the main F<.pm>). Invalid JSON logs a warning
and behaves like an empty object. Root keys such as C<author>, C<abstract>,
C<license>, C<min_perl_version>, C<module_name>, C<version_from>, and C<exe_files>
are copied into F<Makefile.PL> C<WriteMakefile(...)> when set (see L</CONFIGURATION FILE>).
=item *
Patches F<Makefile.PL>: C<META_MERGE> (C<repository> and C<bugtracker> URLs), and
a marked C<MY::postamble> block (between C<# BEGIN PREPARE4RELEASE_POSTAMBLE> and
C<# END PREPARE4RELEASE_POSTAMBLE>) that runs C<pod2github> when C<--github> or
C<--gitlab> was used (else C<pod2markdown>), then F<maint/inject-readme-badges.pl>
(a standalone Perl script regenerated each run, core modules only) so C<make README.md>
reapplies the same shields without depending on C<App::prepare4release>. The block
is refreshed on each run to match the current C<pod2*> choice; the script embeds the
frozen badge Markdown for the chosen C<--github> / C<--gitlab> / C<--cpan> flags.
=item *
When C<--github> or C<--gitlab> is set, ensures CI workflow files exist (see
L</Continuous integration>).
=item *
Regenerates F<README.md> from the F<VERSION_FROM> module (C<make README.md> when
F<Makefile> exists, otherwise C<pod2github> or C<pod2markdown>), then injects
Markdown shield lines (C<[](link)>) into F<README.md> after the first
title block (runs of C<#> headings) or before C<# NAME> when that is the first
heading. The F<Makefile.PL> postamble runs F<maint/inject-readme-badges.pl> after
C<pod2github>/C<pod2markdown> so badges stay in sync without a runtime dependency
on this distribution. Strips any
legacy badge block from POD after C<__END__>. License and minimum Perl badges
are always added; with C<--cpan>, also Repology, CPAN version, and cpants. The
GitHub Actions CI badge is added only with C<--github>; the GitLab pipeline badge
only with C<--gitlab> (host from C<git.server>, else from C<git.repo> URL, else
C<gitlab.com>). License shield (always blue) uses the same key as ExtUtils::MakeMaker
(F<Makefile.PL> C<LICENSE>), or the type inferred from a root F<LICENSE> file when
present; the link is the repository F<LICENSE> blob when that file exists and
C<--github> or C<--gitlab> is set (branch from C<git.default_branch>, default
C<main>), otherwise the usual canonical license URL. Minimum Perl on the shield
comes from C<min_perl_version> / C<perl_min> in the JSON file, else
F<Makefile.PL> C<MIN_PERL_VERSION>, else the stricter of makefile and main module
(as for CI).
=item *
Creates author tests under F<xt/author/> when missing: C<pod.t> (L<Test::Pod>),
C<eol.t> (L<Test::EOL>), C<pod-coverage.t> (L<Test::Pod::Coverage>), using
L<Test2::V1>.
=item *
With C<--cpan>, after the steps above: ensures F<LICENSE> exists. The license
I<type> is taken from F<Makefile.PL> C<LICENSE> (via the same snippet scan as
elsewhere in this tool); if that is missing, I<perl> (same terms as Perl 5) is
assumed. The file text is downloaded from official upstream sources (for
C<perl>, the F<Artistic> and F<Copying> files from the Perl 5 repository; for
C<apache_2>, C<mit>, C<gpl_3>, etc., the canonical license URLs). If a fetch
fails, a short built-in fallback is written. If F<README> is missing but
F<README.md> exists, writes a short stub F<README> pointing readers to
F<README.md>. Creates a default F<MANIFEST.SKIP> when none is present (skipping
F<blib/>, F<cover_db/>, F<nytprof/>, tarballs, F<.git/>, etc.); runs
C<perl Makefile.PL>, copies F<MYMETA.*> to F<META.*>, and C<make manifest> so
F<MANIFEST> matches the tree for CPAN packaging.
=item *
Warns when any F<t/*.t> or F<xt/**/*.t> file starts with C<use Test::More> or
C<use Test::Most> (legacy assertion frameworks). Prefer L<Test2::V1> or
L<Test2::Tools::Spec>.
=item *
Scans F<lib/>, F<bin/>, F<maint/>, F<t/>, and optionally F<xt/> for C<use> /
C<require> and compares with C<PREREQ_PM> / C<TEST_REQUIRES> in F<Makefile.PL>.
Core modules for the target minimum Perl are skipped unless a minimum module
version is given on the C<use> line (see L<Module::CoreList>). By default only a
warning is printed; C<--sync-deps> or C<dependencies.sync> in
F<prepare4release.json> updates F<Makefile.PL> and appends to F<cpanfile> when
present. C<dependencies.skip> disables the check.
=back
=head1 README badge injector (F<maint/inject-readme-badges.pl>)
The C<MY::postamble> fragment cannot hold large, self-contained Perl I<sub>s:
C<ExtUtils::MakeMaker> expects that section to expand into Makefile rules, and
keeping badge logic only in F<Makefile.PL> would either duplicate a lot of text
or imply loading this distribution at C<make README.md> time. Instead,
C<prepare4release> writes F<maint/inject-readme-badges.pl>, a small, generated
program (core modules only) that strips prior shield lines and inserts the
frozen Markdown block computed on the last run (same flags as C<--github> /
C<--gitlab> / C<--cpan>). Downstream distributions should I<commit> that file
with the rest of the tree so C<make README.md> works in a clean clone and the
file is included in the CPAN tarball like any other tracked asset. Re-run
C<prepare4release> after changing repository URLs, license, or badge-related
options so the script and F<README.md> stay consistent. No runtime dependency on
C<App::prepare4release> is added to the target module.
=head1 CONFIGURATION FILE
File name: F<prepare4release.json> (in the distribution root).
An empty file or whitespace-only file is treated as an empty JSON object C<{}>.
Invalid JSON logs a warning and is treated as C<{}>.
=over 4
( run in 1.427 second using v1.01-cache-2.11-cpan-8f98c5d2c55 )