App-prepare4release

 view release on metacpan or  search on metacpan

lib/App/prepare4release.pm  view on Meta::CPAN

sub run {
	my $readme = 'README.md';
	open my $fh, '<:encoding(UTF-8)', $readme
		or die "Cannot open README.md: $!";
	local $/;
	my $text = <$fh> // '';
	close $fh;

	$text = _strip_readme_badge_markdown_block($text);
	my $inner = badge_block();
	my $block = $inner . "\n\n";
	my $new_text = _insert_readme_badges_after_regen( $text, $block );
	return if $new_text eq $text;

	open my $out, '>:encoding(UTF-8)', $readme
		or die "Cannot write README.md: $!";
	print {$out} $new_text;
	close $out;
	return;
}

run() if !caller;

1;
P4R_INJECT_TMPL
}

sub _render_inject_readme_badges_pl {
	my ( $class, $inner ) = @_;
	$inner = '' unless defined $inner;
	my $marker = '__P4R_BADGE_INNER_BODY__';
	croak "inject-readme-badges.pl: badge text must not contain $marker"
		if index( $inner, $marker ) >= 0;
	my $t = $class->_inject_readme_badges_pl_template;
	my $i = index( $t, $marker );
	croak 'inject-readme-badges.pl: template missing inner placeholder'
		if $i < 0;
	substr( $t, $i, length($marker), $inner );
	return $t;
}

sub write_inject_readme_badges_script {
	my ( $class, $cwd, $inner, $verbose ) = @_;
	my $rel  = $class->inject_readme_badges_relpath;
	my @segs = split qr{/}, $rel;
	my $file = pop @segs;
	my $path = File::Spec->catfile( $cwd, @segs, $file );
	make_path( File::Spec->catfile( $cwd, @segs ) ) if @segs;

	my $text = $class->_render_inject_readme_badges_pl($inner);
	open my $out, '>:encoding(UTF-8)', $path
		or croak "Cannot write '$path': $!";
	print {$out} $text;
	close $out;
	chmod 0755, $path or warn "[prepare4release] chmod 0755 '$path': $!\n";
	warn "[prepare4release] wrote $rel (standalone README badge injector)\n"
		if $verbose;
	return;
}

# Makefile fragment: pod2* then inject README shields via generated script (no App::pm dep).
sub _postamble_block {
	my ( $class, $opts ) = @_;
	$opts = {} unless ref $opts eq 'HASH';
	my $tab = "\t";
	my $want_pod2github = $opts->{github} || $opts->{gitlab};
	my $pod_cmd         = $want_pod2github ? 'pod2github' : 'pod2markdown';
	my $inj             = $class->inject_readme_badges_relpath;

	return <<"EOF";
# BEGIN PREPARE4RELEASE_POSTAMBLE
sub MY::postamble {
  return '' if !-e '.git';
  <<'PREPARE4RELEASE_POD2README';
pure_all :: README.md

README.md : \$(VERSION_FROM)
${tab}$pod_cmd \$< \$@
${tab}\$(PERL) $inj
PREPARE4RELEASE_POD2README
}
# END PREPARE4RELEASE_POSTAMBLE
EOF
}

sub makefile_has_pod2github {
	my ( $class, $content ) = @_;
	return $content =~ /pod2github\b/;
}

sub makefile_has_pod2markdown {
	my ( $class, $content ) = @_;
	return $content =~ /pod2markdown\b/;
}

sub _replace_marked_postamble_block {
	my ( $class, $content, $new_block ) = @_;
	return $content
		unless $content =~ /^\# BEGIN PREPARE4RELEASE_POSTAMBLE/m;
	my $out = $content;
	# Line endings: require \r?\n so CRLF files (common on Windows / some editors)
	# still match; a strict \n-only pattern leaves the block unchanged and pod2*
	# never updates.
	# No /x: it would strip spaces in "# BEGIN ..." and treat # as comments. The pattern
	# must be a single line: multiline s{}{} would include literal \n\t from this file.
	$out =~ s/^\# BEGIN PREPARE4RELEASE_POSTAMBLE\s*\r?\n.*?^\# END PREPARE4RELEASE_POSTAMBLE\s*\r?\n?/$new_block/ms;
	return $out;
}

sub _replace_legacy_my_postamble_heredoc {
	my ( $class, $content, $new_block ) = @_;
	my $re = qr/
		^sub \s+ MY::postamble \s* \{
		[\s\S]*?
		<<'(?:POD2README|PREPARE4RELEASE_POD2README)'
		[\s\S]*?
		^(?:POD2README|PREPARE4RELEASE_POD2README)\s*$
		\}
	/mx;
	my $out = $content;
	return $out unless $out =~ $re;

lib/App/prepare4release.pm  view on Meta::CPAN

F<Makefile> exists, otherwise C<pod2github> or C<pod2markdown>), then injects
Markdown shield lines (C<[![Alt](image)](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

=item C<author>

Optional. Copied into F<Makefile.PL> C<AUTHOR> (distinct from C<git.author>).

=item C<abstract>

Optional. Copied into F<Makefile.PL> C<ABSTRACT>.

=item C<abstract_from>

Optional. Copied into F<Makefile.PL> C<ABSTRACT_FROM>.

=item C<license>

Optional. Copied into F<Makefile.PL> C<LICENSE>.

=item C<exe_files>

Optional. JSON array of paths; copied into F<Makefile.PL> C<EXE_FILES>.

=item C<module_name>

Optional. Perl package (e.g. C<My::Module>). If omitted, taken from the
C<VERSION_FROM> module's C<package> line, from C<NAME> in F<Makefile.PL>, or from
the first C<lib/**/*.pm> file. If set, also written to F<Makefile.PL> C<NAME>.

=item C<name>

Optional. Alternative to C<module_name> for F<Makefile.PL> C<NAME> when
C<module_name> is absent.

=item C<version_from>

Optional. Path written to F<Makefile.PL> C<VERSION_FROM> when set.

=item C<version>

Optional. If omitted, taken from C<$VERSION> in the resolved main module file.



( run in 0.926 second using v1.01-cache-2.11-cpan-364913b4093 )