| File: | blib/lib/App/makefilepl2cpanfile.pm |
| Coverage: | 98.9% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::makefilepl2cpanfile; | |||||
| 2 | ||||||
| 3 | 18 18 18 | 1625129 12 150 | use strict; | |||
| 4 | 18 18 18 | 19 13 281 | use warnings; | |||
| 5 | # Nothing is imported: every external function is called by its full name, | |||||
| 6 | # so the only subroutines in this package are its own. (Importing put | |||||
| 7 | # Load, Dump, home, path, croak, carp and Readonly into the namespace.) | |||||
| 8 | 18 18 18 | 22 12 68 | use Carp (); | |||
| 9 | 18 18 18 | 1251 8456 114 | use Readonly (); | |||
| 10 | 18 18 18 | 681 7615 78 | use Path::Tiny (); | |||
| 11 | 18 18 18 | 1168 74789 112 | use Params::Get (); | |||
| 12 | 18 18 18 | 1476 13409 90 | use YAML::Tiny (); | |||
| 13 | 18 18 18 | 2972 39932 149 | use File::HomeDir (); | |||
| 14 | 18 18 18 | 30 10 26375 | use Fcntl (); | |||
| 15 | ||||||
| 16 - 24 | =head1 NAME App::makefilepl2cpanfile - Convert Makefile.PL to a cpanfile automatically =head1 VERSION This document describes App::makefilepl2cpanfile version 0.06. =cut | |||||
| 25 | ||||||
| 26 | our $VERSION = '0.06'; | |||||
| 27 | ||||||
| 28 | # ----------------------------------------------------------------------- | |||||
| 29 | # Constants | |||||
| 30 | # ----------------------------------------------------------------------- | |||||
| 31 | ||||||
| 32 | # Default author/developer tools added to the 'develop' phase when | |||||
| 33 | # with_develop is true and no user config file overrides them. | |||||
| 34 | Readonly::Hash my %DEFAULT_DEVELOP => ( | |||||
| 35 | 'Devel::Cover' => 0, | |||||
| 36 | 'Perl::Critic' => 0, | |||||
| 37 | 'Test::Pod' => 0, | |||||
| 38 | 'Test::Pod::Coverage' => 0, | |||||
| 39 | ); | |||||
| 40 | ||||||
| 41 | # Maps each Makefile.PL simple-dependency key to its cpanfile phase name. | |||||
| 42 | # All of these are treated as 'requires' relationships. | |||||
| 43 | Readonly::Hash my %PHASE_MAP => ( | |||||
| 44 | BUILD_REQUIRES => 'build', | |||||
| 45 | CONFIGURE_REQUIRES => 'configure', | |||||
| 46 | PREREQ_PM => 'runtime', | |||||
| 47 | TEST_REQUIRES => 'test', | |||||
| 48 | ); | |||||
| 49 | ||||||
| 50 | # Valid cpanfile phase names recognised inside 'prereqs => { ... }' blocks. | |||||
| 51 | Readonly::Hash my %VALID_PHASE => map { $_ => 1 } | |||||
| 52 | qw(runtime configure build test develop); | |||||
| 53 | ||||||
| 54 | # Valid cpanfile relationship keywords inside each phase block. | |||||
| 55 | Readonly::Hash my %VALID_REL => map { $_ => 1 } | |||||
| 56 | qw(requires recommends suggests conflicts); | |||||
| 57 | ||||||
| 58 | # Canonical emit order for non-runtime phases (runtime is special-cased at | |||||
| 59 | # the top level per cpanfile convention). | |||||
| 60 | Readonly::Array my @PHASE_ORDER => qw(configure build test develop); | |||||
| 61 | ||||||
| 62 | # Within each phase block, emit relationships in this order. | |||||
| 63 | Readonly::Array my @REL_ORDER => qw(requires recommends suggests conflicts); | |||||
| 64 | ||||||
| 65 | # META spec 1.x top-level keys (e.g. directly under META_MERGE) and the | |||||
| 66 | # phase and relationship each one stands for. | |||||
| 67 | Readonly::Hash my %LEGACY_KEY => ( | |||||
| 68 | requires => [ 'runtime', 'requires' ], | |||||
| 69 | build_requires => [ 'build', 'requires' ], | |||||
| 70 | configure_requires => [ 'configure', 'requires' ], | |||||
| 71 | recommends => [ 'runtime', 'recommends' ], | |||||
| 72 | suggests => [ 'runtime', 'suggests' ], | |||||
| 73 | conflicts => [ 'runtime', 'conflicts' ], | |||||
| 74 | ); | |||||
| 75 | ||||||
| 76 | # First line of every generated cpanfile. | |||||
| 77 | Readonly::Scalar my $HEADER => '# Generated from Makefile.PL using makefilepl2cpanfile'; | |||||
| 78 | ||||||
| 79 | # A CPAN module name: ASCII only (as PAUSE requires), so look-alike | |||||
| 80 | # characters from other scripts cannot smuggle in a different module. | |||||
| 81 | # Possessive quantifiers: no backtracking on hostile input. | |||||
| 82 | Readonly::Scalar my $MODULE_NAME_RE => qr/ | |||||
| 83 | \A | |||||
| 84 | [A-Za-z_] \w*+ # first part: letter or '_', then word characters | |||||
| 85 | (?: :: \w++ )*+ # further parts, each after '::' | |||||
| 86 | \z | |||||
| 87 | /xa; | |||||
| 88 | ||||||
| 89 | # The whole value after '=>': a single- or double-quoted string, or a bare | |||||
| 90 | # token. The complete token is captured so that it is validated as a | |||||
| 91 | # unit - capturing only a numeric prefix would turn '1e3' into '1'. | |||||
| 92 | Readonly::Scalar my $VALUE_TOKEN_RE => qr/ | |||||
| 93 | (?: | |||||
| 94 | ' ([^'\n]*+) ' # $1: single-quoted | |||||
| 95 | | " ([^"\n]*+) " # $2: double-quoted | |||||
| 96 | | ([^\s,}\#'"]++) # $3: bare token, up to a separator | |||||
| 97 | ) | |||||
| 98 | /x; | |||||
| 99 | ||||||
| 100 | # One dependency entry: a quoted module name (quotes must match), '=>', | |||||
| 101 | # then an optional value token. Compiled once here: interpolating | |||||
| 102 | # $VALUE_TOKEN_RE inside the per-line loop made Perl re-check the pattern | |||||
| 103 | # on every line. | |||||
| 104 | Readonly::Scalar my $PAIR_RE => qr/ | |||||
| 105 | (?: | |||||
| 106 | ' ([^'\n]++) ' # $1: single-quoted name | |||||
| 107 | | " ([^"\n]++) " # $2: double-quoted name (quotes must match) | |||||
| 108 | ) | |||||
| 109 | \s* => \s* | |||||
| 110 | $VALUE_TOKEN_RE? # $3-$5: the value, if any | |||||
| 111 | /x; | |||||
| 112 | ||||||
| 113 | # A version number: optional 'v', ASCII digits, '.' and '_', at least one | |||||
| 114 | # digit. The lookahead finds a digit lazily and the main match is | |||||
| 115 | # possessive, so the check is linear even on long hostile strings. | |||||
| 116 | Readonly::Scalar my $VERSION_RE => qr/ | |||||
| 117 | \A | |||||
| 118 | v?+ # optional leading 'v' | |||||
| 119 | (?= [0-9._]*? [0-9] ) # at least one digit (found lazily: linear) | |||||
| 120 | [0-9._]++ # nothing but digits, '.' and '_' | |||||
| 121 | \z | |||||
| 122 | /x; | |||||
| 123 | ||||||
| 124 | # A version requirement: a single version (as above), or a CPAN::Meta | |||||
| 125 | # version range - comma-separated versions each with a comparison operator, | |||||
| 126 | # e.g. '>= 1.2, < 2.0'. Only digits, '.', '_', 'v', the operators, ',' and | |||||
| 127 | # spaces can match, so a requirement can never close the quoted literal it | |||||
| 128 | # is written into. Every quantifier is possessive: linear on any input. | |||||
| 129 | Readonly::Scalar my $REQUIREMENT_RE => qr/ | |||||
| 130 | \A | |||||
| 131 | (?: (?: >= | <= | == | != | > | < ) \s*+ )?+ # optional operator | |||||
| 132 | v?+ (?= [0-9._]*? [0-9] ) [0-9._]++ # a version | |||||
| 133 | (?: | |||||
| 134 | \s*+ , \s*+ | |||||
| 135 | (?: >= | <= | == | != | > | < ) \s*+ # further parts need an operator | |||||
| 136 | v?+ (?= [0-9._]*? [0-9] ) [0-9._]++ | |||||
| 137 | )*+ | |||||
| 138 | \z # no space before or after the whole value | |||||
| 139 | /x; | |||||
| 140 | ||||||
| 141 | # Everything on a line up to and including its first '#' that is not | |||||
| 142 | # inside a quoted string. A quote with no partner is taken as a plain | |||||
| 143 | # character, exactly as a left-to-right scan for quoted strings would. | |||||
| 144 | Readonly::Scalar my $TO_COMMENT_RE => qr/ | |||||
| 145 | \A | |||||
| 146 | (?: | |||||
| 147 | [^'"\#]++ # ordinary text | |||||
| 148 | | ' [^']*+ ' # a single-quoted string (may contain '#') | |||||
| 149 | | " [^"]*+ " # a double-quoted string (may contain '#') | |||||
| 150 | | ['"] # a quote with no partner: an ordinary character | |||||
| 151 | )*+ | |||||
| 152 | \# # the first '#' outside quotes | |||||
| 153 | /x; | |||||
| 154 | ||||||
| 155 | # The four simple keys, for a single pass over the content. | |||||
| 156 | Readonly::Scalar my $SIMPLE_KEY_RE => qr/ | |||||
| 157 | (?: BUILD_REQUIRES | CONFIGURE_REQUIRES | PREREQ_PM | TEST_REQUIRES ) | |||||
| 158 | /x; | |||||
| 159 | ||||||
| 160 | # Openers of dependency blocks: each captures its key in $1 and ends with | |||||
| 161 | # the '{', whose block _blocks() then takes from the matching-brace table. | |||||
| 162 | Readonly::Scalar my $SIMPLE_OPEN_RE => qr/ \b ($SIMPLE_KEY_RE) \s*+ => \s*+ \{ /x; | |||||
| 163 | Readonly::Scalar my $PREREQS_OPEN_RE => qr/ \b (prereqs) \s*+ => \s*+ \{ /x; | |||||
| 164 | # A child of a prereqs or phase block: a phase or relationship name. | |||||
| 165 | Readonly::Scalar my $CHILD_OPEN_RE => qr/ \b (\w++) \s*+ => \s*+ \{ /x; | |||||
| 166 | # The META spec 1.x top-level keys of %LEGACY_KEY, optionally quoted. | |||||
| 167 | Readonly::Scalar my $LEGACY_OPEN_RE => qr/ | |||||
| 168 | \b ( requires | build_requires | configure_requires | recommends | suggests | conflicts ) | |||||
| 169 | ['"]? \s*+ => \s*+ \{ | |||||
| 170 | /x; | |||||
| 171 | ||||||
| 172 | # The text of a comment without its outer whitespace: runs of whitespace | |||||
| 173 | # each followed by non-whitespace, taken possessively, so trailing | |||||
| 174 | # whitespace is left out without being rescanned. (The usual | |||||
| 175 | # s{\s+\z}{} and (.*\S) forms retry from every position inside a long | |||||
| 176 | # whitespace run and are quadratic in its length.) | |||||
| 177 | Readonly::Scalar my $TRIMMED_RE => qr/ | |||||
| 178 | \s*+ # leading whitespace, skipped | |||||
| 179 | ( (?: \s*+ \S++ )++ ) # $1: from the first to the last non-space | |||||
| 180 | /x; | |||||
| 181 | ||||||
| 182 | # The opening of a develop block in an existing cpanfile. | |||||
| 183 | Readonly::Scalar my $DEVELOP_OPEN_RE => qr/ | |||||
| 184 | on \s+ ["']develop["'] \s* => \s* sub \s* \{ | |||||
| 185 | /x; | |||||
| 186 | ||||||
| 187 | # One entry of a develop block: relationship keyword, quoted module name, | |||||
| 188 | # optional quoted version. Quotes must match on both. | |||||
| 189 | Readonly::Scalar my $DEVELOP_ENTRY_RE => qr/ | |||||
| 190 | \b (requires|recommends|suggests|conflicts) \s+ # $1: relationship | |||||
| 191 | (?: ' ([^'\n]++) ' | " ([^"\n]++) " ) # $2 or $3: module name | |||||
| 192 | (?: | |||||
| 193 | \s* , \s* | |||||
| 194 | (?: ' ([^'\n]*+) ' | " ([^"\n]*+) " ) # $4 or $5: version | |||||
| 195 | )? | |||||
| 196 | /x; | |||||
| 197 | ||||||
| 198 | # Characters stripped from comments before they are written out: C0/C1 | |||||
| 199 | # controls other than TAB (a CR can visually overwrite a line) and the | |||||
| 200 | # Unicode bidirectional controls used by "Trojan Source" (CVE-2021-42574) | |||||
| 201 | # to make text display differently from how it is read. | |||||
| 202 | Readonly::Scalar my $UNSAFE_COMMENT_CHARS_RE => | |||||
| 203 | qr/[\x00-\x08\x0A-\x1F\x7F-\x9F\x{061C}\x{200E}\x{200F}\x{202A}-\x{202E}\x{2066}-\x{2069}]/x; | |||||
| 204 | ||||||
| 205 - 654 | =head1 SYNOPSIS
Create or refresh the F<cpanfile> of the project in the current directory:
use App::makefilepl2cpanfile;
use Path::Tiny;
my $text = App::makefilepl2cpanfile::generate();
path('cpanfile')->spew_utf8($text);
Keep the hand-written C<develop> section of an existing F<cpanfile>:
my $old = path('cpanfile')->slurp_utf8;
my $text = App::makefilepl2cpanfile::generate(existing => $old);
path('cpanfile')->spew_utf8($text);
Make a F<cpanfile> for another project, with no developer tools added.
The result is the same on every computer, which is useful in CI:
my $text = App::makefilepl2cpanfile::generate({
makefile => '/path/to/other/project/Makefile.PL',
with_develop => 0,
});
List every dependency without making a F<cpanfile>:
my $deps = App::makefilepl2cpanfile::parse_prereqs(
path('Makefile.PL')->slurp_utf8
);
for my $phase (sort keys %{$deps}) {
for my $rel (sort keys %{ $deps->{$phase} }) {
for my $module (sort keys %{ $deps->{$phase}{$rel} }) {
print "$phase $rel $module\n";
}
}
}
Check in a test that the committed F<cpanfile> is up to date:
use Test::More;
is(
App::makefilepl2cpanfile::generate(existing => path('cpanfile')->slurp_utf8),
path('cpanfile')->slurp_utf8,
'cpanfile matches Makefile.PL',
);
From the command line (see L<makefilepl2cpanfile>):
makefilepl2cpanfile # write ./cpanfile
makefilepl2cpanfile --dry-run # print it instead
makefilepl2cpanfile --diff # show what would change
=head1 DESCRIPTION
A Perl distribution lists the modules it needs in its F<Makefile.PL>.
Many tools (for example C<cpanm --installdeps .> and CI systems) prefer
a F<cpanfile> instead. This module reads a F<Makefile.PL> and writes the
same list in F<cpanfile> form, so you only keep the list in one place.
The F<Makefile.PL> is B<never run>. It is read as plain text and
searched for the parts that list dependencies. This makes the tool safe
to use on code you do not trust, but it also means that it cannot see
dependencies that are computed while the program runs (see
L</COMMON PITFALLS>).
Reading takes time in proportion to the size of the input, however it is
written, so a hostile F<Makefile.PL> or F<cpanfile> (for example one with
thousands of unclosed braces) cannot make the tool hang.
=head2 What it reads
=over 4
=item * C<PREREQ_PM>, C<BUILD_REQUIRES>, C<TEST_REQUIRES> and
C<CONFIGURE_REQUIRES>. These become C<requires> lines in the C<runtime>,
C<build>, C<test> and C<configure> phases.
=item * C<prereqs =E<gt> { PHASE =E<gt> { RELATIONSHIP =E<gt> { ... } } }>
blocks (the CPAN::Meta::Spec version 2 layout), wherever they appear -
also inside C<META_MERGE>.
=item * The older version 1 layout, directly inside C<META_MERGE>:
C<requires>, C<recommends>, C<suggests> and C<conflicts> become lines of
that kind in the C<runtime> phase; C<build_requires> and
C<configure_requires> become C<requires> lines in the C<build> and
C<configure> phases.
=item * C<MIN_PERL_VERSION>, which becomes C<requires 'perl', 'VERSION';>.
=item * A C<#> comment after an entry. It is copied to the F<cpanfile>.
=back
=head2 What it writes
# Generated from Makefile.PL using makefilepl2cpanfile
requires 'perl', '5.010';
requires 'Moo', '2.000'; # object system
requires 'Try::Tiny';
recommends 'JSON::MaybeXS';
on 'test' => sub {
requires 'Test::More', '0.98';
};
on 'develop' => sub {
requires 'Perl::Critic';
};
Runtime dependencies come first, without an C<on> block. The other
phases follow in this order: C<configure>, C<build>, C<test>, C<develop>.
Inside each phase, C<requires> lines come first, then C<recommends>, then
C<suggests>, and the modules are in alphabetical order. The same input
always gives exactly the same output.
=head1 CONFIGURATION
When C<with_develop> is true (the default), some developer tools are
added to the C<develop> phase. By default these are L<Devel::Cover>,
L<Perl::Critic>, L<Test::Pod> and L<Test::Pod::Coverage>.
To choose your own list, create the file
F<~/.config/makefilepl2cpanfile.yml>:
develop:
Perl::Critic: 0
Devel::Cover: 0
My::Extra::Tool: '1.00'
The number after each name is the minimum version; C<0> means "any
version". Your list B<replaces> the default list; it is not added to it.
Names that are not valid module names, and versions that are not version
numbers, are ignored with a warning, so a mistake in this file can never
put unexpected code into your F<cpanfile>.
=head1 DATA STRUCTURE
C<parse_prereqs()> returns a hash reference with three levels:
{
PHASE => {
RELATIONSHIP => {
'Module::Name' => { version => '1.0', comment => 'why it is needed' },
},
},
}
=over 4
=item * PHASE is one of C<runtime>, C<configure>, C<build>, C<test> or
C<develop>.
=item * RELATIONSHIP is one of C<requires>, C<recommends>, C<suggests> or
C<conflicts>.
=item * C<version> is the version requirement exactly as it was written:
a single version (C<'1.60'> stays C<'1.60'>) or a version range such as
C<'E<gt>= 1.2, E<lt> 2.0'>. It is C<0> when there is no requirement.
=item * C<comment> is the text of the C<#> comment after the entry, or
C<undef> when there is none.
=back
A phase or relationship with no modules is not present at all.
=head1 ENCODING
=over 4
=item * B<The Makefile.PL file> is read as UTF-8. Comments may contain
any printable Unicode text, including accented letters, emoji and
combining marks; they are copied to the output unchanged. Control
characters (other than TAB) and Unicode direction-override characters
are removed from comments, so that the generated file cannot be made to
look different from what it really contains. If the file is not valid UTF-8 you get a
warning and processing continues (see L</generate(%args)>).
=item * B<The returned cpanfile text> is a Perl character string. Write
it with a UTF-8 output layer, for example C<path(...)-E<gt>spew_utf8> or
C<binmode $fh, ':encoding(UTF-8)'>. Otherwise Perl warns
"Wide character in print" when a comment contains non-ASCII text.
=item * B<The existing argument> and B<the content argument of
parse_prereqs()> must be character strings: decode them first (for
example with C<slurp_utf8>). Raw bytes work for plain ASCII, but a
non-ASCII comment would come back as separate bytes.
=item * B<Module names> must be plain ASCII (A-Z, a-z, 0-9, C<_> and
C<::>), as CPAN requires. A name with any other character is ignored.
This also stops look-alike names, such as C<Test::More> written with a
Cyrillic letter.
=item * B<Version numbers> may use only the ASCII digits 0-9, C<.>,
C<_> and a leading C<v>, and must contain at least one digit. A version
range joins such numbers, each after one of the operators C<E<gt>=>,
C<E<lt>=>, C<==>, C<!=>, C<E<gt>> or C<E<lt>>, with commas. The whole
value is checked: C<'1.0-TRIAL'> is not shortened to C<'1.0'>. Anything
else is treated as "no minimum version".
=item * B<The YAML configuration file> is read as UTF-8; the same rules
for names and versions apply.
=item * B<File names> are passed to the operating system unchanged.
Names with spaces, non-ASCII letters or shell characters such as C<;>
and C<|> are safe: no shell is ever used.
=back
=head1 METHODS
=head2 generate(%args)
=head3 PURPOSE
Reads a F<Makefile.PL> and returns the text of a matching F<cpanfile>.
The F<Makefile.PL> is read as text and is never run.
=head3 ARGUMENTS
Give the arguments as a list of name/value pairs, or as one hash
reference. All of them are optional.
=over 4
=item * C<content> - the text of a F<Makefile.PL>, for example from
L</read_makefile($path)>. When it is given it is used as it is, and
C<makefile> is ignored and nothing is read. Use it to read the file once
and use the same text for more than one purpose.
=item * C<makefile> - the path of the F<Makefile.PL> to read. Default:
C<'Makefile.PL'> in the current directory. Anything that turns into a
path when used as a string is accepted, such as a L<Path::Tiny> object.
A filehandle or another kind of reference is refused with
C<Cannot read>.
=item * C<existing> - the text of your current F<cpanfile>. Default:
C<''> (none). Only its C<on 'develop' =E<gt> sub { ... }> section is
used. Every C<requires>, C<recommends>, C<suggests> and C<conflicts>
line in that section is copied to the new text, so your hand-written developer
dependencies are kept. Lines that are commented out are not copied.
An entry whose name is not a valid module name is dropped. An entry
whose version is not a version number or version range is kept without a
version, and you get a warning.
=item * C<with_develop> - true or false. Default: true. When true, the
developer tools from the configuration file (see L</CONFIGURATION>), or
the default tools, are added to the C<develop> phase as C<requires>. A
tool that is already in the develop phase, from the F<Makefile.PL> or
from C<existing>, is left exactly as it is.
=back
=head3 RETURNS
A string: the complete F<cpanfile>. It always starts with the line
C<# Generated from Makefile.PL using makefilepl2cpanfile> and always ends
with exactly one newline. The layout is described in
L</What it writes>. A F<Makefile.PL> with no dependencies gives just
the first line.
=head3 SIDE EFFECTS
=over 4
=item * Reads the C<makefile> file.
=item * When C<with_develop> is true, reads
F<~/.config/makefilepl2cpanfile.yml> if it exists.
=item * Never writes, creates or deletes any file.
=item * May print warnings (see MESSAGES below).
=item * Does not change the caller's C<$@>, C<$!> or C<$_>.
=back
=head3 USAGE EXAMPLE
use App::makefilepl2cpanfile;
use Path::Tiny;
my $cpanfile = path('cpanfile');
my $text = App::makefilepl2cpanfile::generate(
makefile => 'Makefile.PL',
existing => $cpanfile->exists ? $cpanfile->slurp_utf8 : '',
with_develop => 1,
);
$cpanfile->spew_utf8($text);
=head3 HOW IT WORKS
=over 4
=item 1. Check that C<makefile> is a readable, regular file.
=item 2. Read it as UTF-8. If that fails because of bad UTF-8, warn and
read the raw bytes instead. Any other read error is passed on.
=item 3. Find C<MIN_PERL_VERSION> and call L</parse_prereqs($content)>.
=item 4. Copy the develop entries from C<existing>, without replacing
entries that came from the F<Makefile.PL>.
=item 5. If C<with_develop> is true, add each configured tool that is
not already in the develop phase.
=item 6. Format and return the text.
=back
=head3 API SPECIFICATION
=head4 Input
{
content => {
type => 'string',
optional => 1,
},
makefile => {
type => 'string',
optional => 1,
min => 1,
default => 'Makefile.PL',
},
existing => {
type => 'string',
optional => 1,
default => '',
},
with_develop => {
type => 'boolean',
optional => 1,
default => 1,
},
}
=head4 Output
{
type => 'string',
matches => qr/\A# Generated from Makefile\.PL using makefilepl2cpanfile\n.*(?<!\n)\n\z/s,
}
=head4 Domains
Each argument, split into groups of values that behave the same way.
"Refused" means the call dies with C<Cannot read '...'>.
makefile
valid: a readable regular file, given as a relative path, an
absolute path, or an object that stringifies to one
(e.g. Path::Tiny). Any characters the file system allows,
including spaces, non-ASCII letters and shell characters.
default: undef or not given -> 'Makefile.PL'
refused: '' and '0' (unless such a file exists), a missing file,
a directory, a device, a FIFO, a symlink loop, a file
without read permission, any reference (filehandle, glob,
array, hash, code)
limits: a file name component up to the file system's NAME_MAX
(usually 255 bytes) is accepted; one byte more is refused.
File size: 0 bytes gives just the header line; there is
no upper limit other than memory.
content
given: used as the Makefile.PL text; makefile is then ignored
and no file is read (so no "Cannot read" is possible)
default: undef or not given -> the makefile file is read
existing
valid: any string. Only the first on 'develop' => sub { ... };
block is used; its closing "};" must start a line (after
optional spaces or tabs). 'develop' or "develop".
default: undef or not given -> ''
ignored: a string without a develop block, a block that is never
closed, a reference (its "HASH(0x...)" text has no block)
entries: 0 or more; each needs a valid module name; a version, if
given, must be a version or version range (see
parse_prereqs)
with_develop
true: any true Perl value, including 'yes' and '0.0'
false: 0, '0', ''
default: undef or not given -> true
combinations
- with_develop false does not stop the existing develop block from
being kept; it only stops tools being added.
- An empty Makefile.PL with an existing develop block gives the
header plus that develop block.
- A configuration file with an empty "develop: {}" adds no tools.
=head3 MESSAGES
Values taken from files or the environment (paths, module names,
versions, parser errors) are shown with control characters and Unicode
direction-override characters replaced by C<\x{..}> escapes, so a
hostile file cannot use a message to send escape sequences to your
terminal.
Errors (the call dies):
Cannot read '$makefile'
The path does not exist, is not a regular file (for example a
directory, a device or a FIFO), cannot be read, or is not a path
at all (for example a filehandle).
What to do: check the path and its permissions.
Failed to parse $cfg_file: $error
The configuration file exists but contains invalid YAML, cannot be
read, or its location cannot be checked (for example "Permission
denied"). A missing file, or something that is not a regular
file, is simply treated as "no configuration".
What to do: fix the YAML or the permissions, or delete the file.
(any other error while reading the Makefile.PL)
A real read error, such as a disk failure, is passed on unchanged.
Warnings (processing continues):
Warning: '$makefile' contains invalid UTF-8; reading as raw bytes: $error
The file is not valid UTF-8. Its raw bytes are used instead.
Note: depending on which optional UTF-8 modules are installed
(Unicode::UTF8, PerlIO::utf8_strict), Path::Tiny may instead
decode the file leniently and print its own warning. Either way
you get a warning, not an error.
What to do: save the Makefile.PL as UTF-8.
Ignoring invalid version for '$module' in existing cpanfile: '$version'
A develop entry in the existing cpanfile has a version that is
not a version number. The entry is kept with no version, so the
bad value cannot change the meaning of the new cpanfile.
No 'develop' key found in $cfg_file; using defaults
The configuration file has no develop: section.
What to do: add one, or delete the file.
Skipping invalid module name in $cfg_file: '$module'
A name in the develop: section is not a valid module name. It is
ignored.
Skipping invalid version for '$module' in $cfg_file: '$version'
A version in the develop: section is not a version number. The
module is kept with no minimum version.
=cut | |||||
| 655 | ||||||
| 656 | sub generate { | |||||
| 657 | # Accept both flat hash and single-hashref calling styles. | |||||
| 658 | 556 | 10795751 | my $args = Params::Get::get_params(undef, \@_); | |||
| 659 | ||||||
| 660 | # File tests and the reads below (including the config file) change | |||||
| 661 | # errno; restore the caller's $! on every exit path. | |||||
| 662 | 555 | 8163 | local $! = 0; | |||
| 663 | ||||||
| 664 | 555 | 853 | my $existing = $args->{existing} // ''; | |||
| 665 | 555 | 554 | my $with_dev = $args->{with_develop} // 1; | |||
| 666 | ||||||
| 667 | # Text supplied directly is used as it is; otherwise read the file. | |||||
| 668 | 555 | 701 | my $content = defined $args->{content} ? $args->{content} : read_makefile($args->{makefile}); | |||
| 669 | 452 | 412 | my $min_perl = _parse_min_perl($content); | |||
| 670 | 452 | 384 | my $deps = parse_prereqs($content); | |||
| 671 | ||||||
| 672 | # Merge the develop block from a pre-existing cpanfile so that | |||||
| 673 | # hand-curated entries (all relationship types) survive regeneration. | |||||
| 674 | # | |||||
| 675 | # Two linear steps instead of one /(.*?)^};/ms match: that lazy match | |||||
| 676 | # re-scanned to the end of the string from every unclosed opener, which | |||||
| 677 | # is quadratic. Premise: any closing line after a later opener is also | |||||
| 678 | # after the first. Conclusion: only the first opener needs trying. | |||||
| 679 | # The closing '};' must start a line, so a '};' inside an inline | |||||
| 680 | # comment does not end the block early. | |||||
| 681 | 452 | 673 | if ($existing =~ /$DEVELOP_OPEN_RE/gx) { | |||
| 682 | 88 | 70 | my $open_end = pos $existing; | |||
| 683 | 88 | 315 | if ($existing =~ /^[ \t]*\};/mgx) { | |||
| 684 | 83 | 156 | my $dev_block = substr $existing, $open_end, $-[0] - $open_end; | |||
| 685 | ||||||
| 686 | # A commented-out line is not an entry. '#' cannot occur in a | |||||
| 687 | # valid module name or version, so stripping to end of line is safe. | |||||
| 688 | 83 | 85 | $dev_block =~ s/\#[^\n]*+//gx; | |||
| 689 | ||||||
| 690 | 83 | 255 | while ($dev_block =~ /$DEVELOP_ENTRY_RE/gx) { | |||
| 691 | # Save immediately: the validation regexes below would reset | |||||
| 692 | # the capture variables. | |||||
| 693 | 2134 | 2518 | my ($rel, $mod, $ver) = ($1, $2 // $3, $4 // $5); | |||
| 694 | 2134 | 2288 | next unless $mod =~ $MODULE_NAME_RE; | |||
| 695 | # SECURITY: the version is written back inside a single-quoted | |||||
| 696 | # literal; a value such as '\' would escape the closing quote and | |||||
| 697 | # turn following entries into executable code. | |||||
| 698 | # length(undef) is undef, so one test covers "absent" and "empty": | |||||
| 699 | # only a non-empty version can be invalid. | |||||
| 700 | 2126 | 1806 | if (length($ver // q{}) && !_valid_requirement($ver)) { | |||
| 701 | 37 | 35 | Carp::carp "Ignoring invalid version for '$mod' in existing cpanfile: '" . _printable($ver) . q{'}; | |||
| 702 | 37 | 2603 | $ver = 0; | |||
| 703 | } | |||||
| 704 | 2126 | 5297 | $deps->{develop}{$rel}{$mod} //= { version => $ver || 0, comment => undef }; | |||
| 705 | } | |||||
| 706 | } | |||||
| 707 | } | |||||
| 708 | ||||||
| 709 | 452 | 340 | if ($with_dev) { | |||
| 710 | 231 | 189 | my $config = _load_develop_config(); | |||
| 711 | ||||||
| 712 | # Premise 1: a listed tool must never be added again or overwritten. | |||||
| 713 | # Premise 2: "listed" means present under any relationship. | |||||
| 714 | # Conclusion: build that set once, then add every tool outside it. | |||||
| 715 | # ($dev is a copy of the reference or a fresh {}, so reading it | |||||
| 716 | # cannot autovivify $deps->{develop} or its relationship hashes.) | |||||
| 717 | 190 | 1852 | my $dev = $deps->{develop} || {}; | |||
| 718 | 190 1125 760 760 | 261 874 1760 528 | my %listed = map { $_ => 1 } map { keys %{ $dev->{$_} || {} } } @REL_ORDER; | |||
| 719 | 190 532 190 | 377 388 193 | for my $mod (grep { !$listed{$_} } keys %{$config}) { | |||
| 720 | 520 | 591 | $deps->{develop}{requires}{$mod} = { version => $config->{$mod}, comment => undef }; | |||
| 721 | } | |||||
| 722 | } | |||||
| 723 | ||||||
| 724 | 411 | 394 | return _emit($deps, $min_perl); | |||
| 725 | } | |||||
| 726 | ||||||
| 727 - 787 | =head2 read_makefile($path)
=head3 PURPOSE
Reads the text of a F<Makefile.PL>, exactly as L</generate(%args)> does
when it is given a C<makefile> argument. Use it together with the
C<content> argument of C<generate()> when you need the same text for
more than one call, so that the file is read only once.
=head3 ARGUMENTS
=over 4
=item * C<$path> - as the C<makefile> argument of L</generate(%args)>:
optional, default C<'Makefile.PL'>, used as a string.
=back
=head3 RETURNS
The file's text as a character string. If the file is not valid UTF-8
you get a warning and its raw bytes instead (see L</generate(%args)>).
=head3 SIDE EFFECTS
Reads the file. May warn. Does not change the caller's C<$@>, C<$!>
or C<$_>.
=head3 USAGE EXAMPLE
my $text = App::makefilepl2cpanfile::read_makefile('Makefile.PL');
my $deps = App::makefilepl2cpanfile::parse_prereqs($text);
my $out = App::makefilepl2cpanfile::generate(content => $text);
=head3 API SPECIFICATION
=head4 Input
{
path => {
type => 'string',
optional => 1,
min => 1,
default => 'Makefile.PL',
position => 0,
},
}
=head4 Output
{
type => 'string',
}
=head3 MESSAGES
The same as the file-reading messages of L</generate(%args)>:
C<Cannot read '$path'> (croak), the invalid-UTF-8 warning, and any
other read error passed on unchanged.
=cut | |||||
| 788 | ||||||
| 789 | sub read_makefile { | |||||
| 790 | # Stringify so a filehandle, glob or other reference is judged by the | |||||
| 791 | # same "Cannot read" guard as a bad path, rather than being accepted by | |||||
| 792 | # -f (which also tests open handles) and then misread as a path. | |||||
| 793 | 558 558 | 2946 725 | my $makefile = "@{[ $_[0] // 'Makefile.PL' ]}"; | |||
| 794 | ||||||
| 795 | # File tests and reads change errno; restore the caller's $! and $@. | |||||
| 796 | 558 | 577 | local $! = 0; | |||
| 797 | 558 | 357 | local $@ = q{}; | |||
| 798 | ||||||
| 799 | 558 | 2943 | Carp::croak "Cannot read '" . _printable($makefile) . q{'} unless -f $makefile && -r _; | |||
| 800 | ||||||
| 801 | 472 | 301 | my $content; | |||
| 802 | 472 472 442 | 317 475 30831 | if (!eval { $content = Path::Tiny::path($makefile)->slurp_utf8; 1 }) { | |||
| 803 | # Only degrade gracefully for encoding errors; re-throw true I/O | |||||
| 804 | # failures so callers can distinguish a corrupt file from an | |||||
| 805 | # unreadable one. Path::Tiny reports I/O failures as | |||||
| 806 | # Path::Tiny::Error objects, whose text includes the file's path: | |||||
| 807 | # matching keywords against that would misread an I/O error in, | |||||
| 808 | # say, ~/src/utf8-tools/ as a decoding problem. So objects are | |||||
| 809 | # always rethrown, and only plain decoder messages are matched. | |||||
| 810 | 30 | 1122 | die $@ if ref $@ || $@ !~ /decode|ill-formed|utf/ix; ## no critic (RequireCarping) re-thrown unchanged, as documented; croak would append a location | |||
| 811 | 13 | 19 | Carp::carp "Warning: '" . _printable($makefile) . "' contains invalid UTF-8; reading as raw bytes: " | |||
| 812 | . _printable($@ =~ s/\n\z//rx); | |||||
| 813 | 13 | 1073 | $content = Path::Tiny::path($makefile)->slurp_raw; | |||
| 814 | } | |||||
| 815 | 454 | 810 | return $content; | |||
| 816 | } | |||||
| 817 | ||||||
| 818 - 953 | =head2 parse_prereqs($content)
=head3 PURPOSE
Finds every dependency listed in the text of a F<Makefile.PL> and returns
them grouped by phase and relationship. This is the parser that
L</generate(%args)> uses; call it directly when you want the data rather
than a F<cpanfile>. The text is never run.
=head3 ARGUMENTS
=over 4
=item * C<$content> - the text of a F<Makefile.PL>, as a character string
(see L</ENCODING>). C<undef> or a reference is treated as text with no
dependencies.
=back
The forms that are recognised are listed in L</What it reads>. These
rules also apply:
=over 4
=item * Only entries whose name is in quotes and is a valid module name
are used. Several entries may be on one line; a C<#> comment at the end
of a line belongs to the last entry on that line.
=item * Commented-out code is ignored, including a whole dependency list
written on one line after a C<#>.
=item * A version must contain at least one digit and use only C<0-9>,
C<.>, C<_> and a leading C<v>; a version range joins such versions, each
after a comparison operator, with commas. Anything else (for example
C<'.'> or C<$VERSION>) means "no minimum version".
=item * When a module appears twice in the same phase and relationship,
the first one wins. The simple keys are read before C<prereqs> blocks.
=item * Relationship hashes inside a C<prereqs> block belong to that
block's phase; only the version 1 keys directly inside C<META_MERGE> are
mapped as described in L</What it reads>.
=item * A comment at the end of a line belongs to the last entry on the
line that is actually kept; an entry dropped for an invalid name does not
take the comment with it.
=back
=head3 RETURNS
A hash reference as described in L</DATA STRUCTURE>. It is empty when
nothing is found.
=head3 USAGE EXAMPLE
use App::makefilepl2cpanfile;
use Path::Tiny;
my $deps = App::makefilepl2cpanfile::parse_prereqs(
path('Makefile.PL')->slurp_utf8
);
# Which test modules are needed, and which minimum versions?
my $test = $deps->{test}{requires} || {};
for my $module (sort keys %{$test}) {
printf "%-30s %s\n", $module, $test->{$module}{version} || 'any';
}
=head3 API SPECIFICATION
=head4 Input
{
content => {
type => 'string',
optional => 1,
position => 0,
},
}
=head4 Output
{
type => 'hashref',
}
=head4 Domains
content
valid: any string (decoded characters; see ENCODING)
empty: '', undef, any reference, text with no dependency lists
-> {} (no warning)
module name (the quoted key of an entry)
valid: ASCII letter or '_' first, then ASCII letters, digits and
'_', in parts joined by '::'. Shortest: one character
('A', '_'). No maximum length.
ignored: '' ; leading digit ('1A') ; '::' at either end ; ':::' ;
'-', space, ';' or the old "'" package separator ;
any non-ASCII character ; a bareword (unquoted) key
version (the value of an entry, and MIN_PERL_VERSION)
valid: the whole value is an optional 'v' followed by ASCII
digits, '.' and '_', with at least one digit:
'1', 1.60, 'v1.2.3', '1.23_01', '5.010001'
range: (entries only, not MIN_PERL_VERSION) such versions, each
after one of >= <= == != > <, joined by commas:
'>= 1.2, < 2.0', '== 1.5', '< 2'. '>= 0' means "any".
zero: '0', 0, '0.0', '0.000', 'v0', 'v0.0.0' -> "no minimum"
(nothing is written)
invalid: -> "no minimum": '.', '_', 'v', '1e3', '1.0-TRIAL',
'1 0', non-ASCII digits, $VERSION, version->parse(...),
spaces around the whole value, and ranges with a missing
comma or operator ('>= 1.2 < 2.0', '1.2, 2.0')
limits: no maximum length
phase / relationship (inside prereqs blocks)
valid: exactly runtime, configure, build, test, develop /
requires, recommends, suggests, conflicts (lower case)
ignored: anything else, including 'Runtime', 'recommend', 'x_foo'
comment (text after '#' on an entry's line)
kept: any printable Unicode: accents, 'ss'-type letters, emoji,
combining marks, right-to-left scripts
removed: control characters other than TAB (for example CR) and
the bidirectional control characters U+061C, U+200E,
U+200F, U+202A-U+202E, U+2066-U+2069, which could make the
generated file display differently from its real content
empty: a comment that is empty after this -> undef
=head3 MESSAGES
None. Text that is not recognised is ignored without a warning.
=cut | |||||
| 954 | ||||||
| 955 | sub parse_prereqs { | |||||
| 956 | 729 | 830038 | my $content = $_[0]; | |||
| 957 | ||||||
| 958 | # The POD contract is "no errors or warnings - unrecognised content is | |||||
| 959 | # silently ignored." Undef and references are not valid Str inputs; return | |||||
| 960 | # {} immediately to avoid "uninitialized value" and "reference used as | |||||
| 961 | # string" warnings from the pattern-match operators below. | |||||
| 962 | 729 | 1002 | return {} if !defined $content || ref $content; | |||
| 963 | ||||||
| 964 | 704 | 396 | my %deps; | |||
| 965 | 704 | 574 | my $comments = _comment_spans($content); | |||
| 966 | ||||||
| 967 | # Every block is found through the matching-brace table, so a block may | |||||
| 968 | # nest to any depth and each scan below is linear: no pattern has to | |||||
| 969 | # match balanced braces, which is what made unclosed openers quadratic. | |||||
| 970 | 704 | 557 | my $pairs = _brace_pairs($content); | |||
| 971 | ||||||
| 972 | # ---- Simple dependency keys (PREREQ_PM, BUILD_REQUIRES, etc.) ---- | |||||
| 973 | # These always map to the 'requires' relationship in their phase. All | |||||
| 974 | # four are found in one pass: each key has its own phase, so the order | |||||
| 975 | # in which blocks are met cannot change which entry wins. | |||||
| 976 | 704 704 | 392 519 | for my $found (@{ _blocks($content, 0, $pairs, $SIMPLE_OPEN_RE) }) { | |||
| 977 | 20768 20768 | 9978 13903 | my ($key, $at, undef, $block) = @{$found}; | |||
| 978 | 20768 | 11173 | next if _in_comment($comments, $at); | |||
| 979 | 20753 | 15020 | _extract_pairs($block, \%deps, $PHASE_MAP{$key}, 'requires'); | |||
| 980 | } | |||||
| 981 | ||||||
| 982 | # ---- Structured 'prereqs' blocks (CPAN Meta Spec style) ---- | |||||
| 983 | # These can appear at the top level of WriteMakefile() or nested inside | |||||
| 984 | # META_MERGE; both are covered by searching the full content for 'prereqs'. | |||||
| 985 | # The [start, end) offset of each block is recorded so that the legacy | |||||
| 986 | # scan below can skip relationship blocks nested inside it. | |||||
| 987 | 704 | 3340 | my @prereqs_spans; | |||
| 988 | 704 704 | 378 464 | for my $prereqs (@{ _blocks($content, 0, $pairs, $PREREQS_OPEN_RE) }) { | |||
| 989 | 20211 20211 | 9253 15477 | my (undef, $at, $prereqs_base, $prereqs_block, $end) = @{$prereqs}; | |||
| 990 | 20211 | 11024 | next if _in_comment($comments, $at); | |||
| 991 | 20208 | 11483 | push @prereqs_spans, [ $at, $end ]; | |||
| 992 | ||||||
| 993 | # Each direct child is a phase name mapping to a relationship hash. | |||||
| 994 | 20208 20208 | 8910 10762 | for my $phase (@{ _blocks($prereqs_block, $prereqs_base, $pairs, $CHILD_OPEN_RE) }) { | |||
| 995 | 20348 20348 | 9036 12082 | my ($phase_name, $phase_at, $phase_base, $phase_block) = @{$phase}; | |||
| 996 | 20348 | 16389 | next unless $VALID_PHASE{$phase_name}; | |||
| 997 | 20340 | 36561 | next if _in_comment($comments, $phase_at); | |||
| 998 | ||||||
| 999 | # Each child of the phase block is a relationship name. | |||||
| 1000 | 20338 20338 | 9260 10743 | for my $relationship (@{ _blocks($phase_block, $phase_base, $pairs, $CHILD_OPEN_RE) }) { | |||
| 1001 | 20351 20351 | 9069 12202 | my ($rel, $rel_at, undef, $rel_block) = @{$relationship}; | |||
| 1002 | 20351 | 15928 | next unless $VALID_REL{$rel}; | |||
| 1003 | 20343 | 36735 | next if _in_comment($comments, $rel_at); | |||
| 1004 | ||||||
| 1005 | 20340 | 11587 | _extract_pairs($rel_block, \%deps, $phase_name, $rel); | |||
| 1006 | } | |||||
| 1007 | } | |||||
| 1008 | } | |||||
| 1009 | ||||||
| 1010 | # ---- Legacy top-level keys (META spec 1.x) ---- | |||||
| 1011 | # e.g. META_MERGE => { recommends => { 'Mod' => 0 } }, mapped through | |||||
| 1012 | # %LEGACY_KEY. Occurrences inside a 'prereqs' block are phase-scoped and | |||||
| 1013 | # have already been handled above, so skip them. | |||||
| 1014 | 704 704 | 3530 483 | for my $found (@{ _blocks($content, 0, $pairs, $LEGACY_OPEN_RE) }) { | |||
| 1015 | 60410 60410 | 27133 43322 | my ($key, $at, undef, $block) = @{$found}; | |||
| 1016 | # The spans come from a scan that resumes after each block, so they | |||||
| 1017 | # are sorted and do not overlap: the same binary search as for | |||||
| 1018 | # comments applies, O(log P) per block instead of a linear scan | |||||
| 1019 | # (which made many legacy blocks times many prereqs blocks quadratic). | |||||
| 1020 | 60410 | 33688 | next if _in_comment(\@prereqs_spans, $at); | |||
| 1021 | 40058 | 21463 | next if _in_comment($comments, $at); | |||
| 1022 | 40050 40050 | 19089 36664 | _extract_pairs($block, \%deps, @{ $LEGACY_KEY{$key} }); | |||
| 1023 | } | |||||
| 1024 | ||||||
| 1025 | 704 | 32976 | return \%deps; | |||
| 1026 | } | |||||
| 1027 | ||||||
| 1028 | # ----------------------------------------------------------------------- | |||||
| 1029 | ||||||
| 1030 | # _brace_pairs | |||||
| 1031 | # | |||||
| 1032 | # Purpose: Pair every '{' in the content with its matching '}', in one | |||||
| 1033 | # left-to-right pass with a stack. | |||||
| 1034 | # Entry: $_[0] - the content (a defined string). | |||||
| 1035 | # Exit: Returns a hashref { offset of '{' => offset of its '}' }. A '{' | |||||
| 1036 | # that is never closed has no entry; a '}' with no '{' before it | |||||
| 1037 | # is ignored. | |||||
| 1038 | # Effects: None. | |||||
| 1039 | # | |||||
| 1040 | # Braces are counted wherever they appear, including inside quoted strings | |||||
| 1041 | # and comments, as the balanced-brace patterns this replaced did. | |||||
| 1042 | sub _brace_pairs { | |||||
| 1043 | 712 | 1234 | my $content = $_[0]; | |||
| 1044 | 712 | 387 | my (%close_of, @open); | |||
| 1045 | 712 | 1727 | while ($content =~ /[{}]/gx) { | |||
| 1046 | 794462 | 516162 | if (substr($content, $-[0], 1) eq '{') { | |||
| 1047 | 672233 | 632858 | push @open, $-[0]; | |||
| 1048 | } elsif (@open) { | |||||
| 1049 | 122227 | 153856 | $close_of{ pop @open } = $-[0]; | |||
| 1050 | } | |||||
| 1051 | } | |||||
| 1052 | 712 | 15628 | return \%close_of; | |||
| 1053 | } | |||||
| 1054 | ||||||
| 1055 | # ----------------------------------------------------------------------- | |||||
| 1056 | ||||||
| 1057 | # _blocks | |||||
| 1058 | # | |||||
| 1059 | # Purpose: Find each "KEY => {" whose '{' is closed, with the text of its | |||||
| 1060 | # block. | |||||
| 1061 | # Entry: $_[0] - text to scan: a substring of the content. | |||||
| 1062 | # $_[1] - offset of that substring in the content. | |||||
| 1063 | # $_[2] - _brace_pairs() of the whole content. | |||||
| 1064 | # $_[3] - opener pattern: captures the key in $1, ends with '{'. | |||||
| 1065 | # Exit: Returns an arrayref of [ key, offset of the key, offset of the | |||||
| 1066 | # block's text, the block's text, offset just past its '}' ]. | |||||
| 1067 | # Offsets are in the whole content. | |||||
| 1068 | # Effects: None. | |||||
| 1069 | # | |||||
| 1070 | # Like a /g match of a balanced-brace pattern, the scan resumes after each | |||||
| 1071 | # block, so a key inside a block that was found is not reported | |||||
| 1072 | # separately. An unclosed '{' is passed over and the scan continues | |||||
| 1073 | # inside it. | |||||
| 1074 | sub _blocks { | |||||
| 1075 | 42662 | 22695 | my ($text, $base, $pairs, $opener_re) = @_; | |||
| 1076 | 42662 | 18725 | my @blocks; | |||
| 1077 | 42662 | 107963 | while ($text =~ /$opener_re/gx) { | |||
| 1078 | 492096 | 415210 | my ($key, $at, $opening) = ($1, $base + $-[0], $base + $+[0] - 1); | |||
| 1079 | 492096 | 582381 | my $closing = $pairs->{$opening} // next; | |||
| 1080 | 142092 | 114868 | push @blocks, [ $key, $at, $opening + 1, substr($text, $opening + 1 - $base, $closing - $opening - 1), $closing + 1 ]; | |||
| 1081 | 142092 | 182195 | pos($text) = $closing + 1 - $base; | |||
| 1082 | } | |||||
| 1083 | 42662 | 27116 | return \@blocks; | |||
| 1084 | } | |||||
| 1085 | ||||||
| 1086 | # ----------------------------------------------------------------------- | |||||
| 1087 | # Private helpers | |||||
| 1088 | # ----------------------------------------------------------------------- | |||||
| 1089 | ||||||
| 1090 | # _extract_pairs | |||||
| 1091 | # | |||||
| 1092 | # Purpose: Parse a raw block of text (the content between the outermost | |||||
| 1093 | # braces of a dependency hash) into module/version/comment triples | |||||
| 1094 | # and store them in the deps structure. Processes line-by-line | |||||
| 1095 | # so that trailing inline comments can be captured before the | |||||
| 1096 | # comment text is discarded. | |||||
| 1097 | # Entry: $_[0] - raw block text (between the outer braces). | |||||
| 1098 | # $_[1] - hashref to populate (the top-level %deps). | |||||
| 1099 | # $_[2] - phase name string (e.g. 'runtime'). | |||||
| 1100 | # $_[3] - relationship string (e.g. 'requires'). | |||||
| 1101 | # Exit: Returns nothing - mutates $_[1] in place. | |||||
| 1102 | # Effects: Modifies the deps hashref; no I/O. | |||||
| 1103 | # | |||||
| 1104 | # First-occurrence-wins: if the same module appears multiple times (e.g. | |||||
| 1105 | # once in PREREQ_PM and once in a prereqs block), the first parsed entry | |||||
| 1106 | # is kept. | |||||
| 1107 | sub _extract_pairs { | |||||
| 1108 | 81164 | 149268 | my ($block, $deps, $phase, $rel) = @_; | |||
| 1109 | ||||||
| 1110 | 81164 | 168438 | for my $line (split /\n/x, $block) { | |||
| 1111 | # Capture any trailing inline comment before stripping it. | |||||
| 1112 | # (.*\S) is O(N): greedy .* scans to end, then gives back trailing | |||||
| 1113 | # spaces one by one until \S anchors on the last non-space char. | |||||
| 1114 | # Avoids the super-linear behaviour of (.+?)\s*$ which re-evaluates | |||||
| 1115 | # \s*$ at every expanded position of the lazy quantifier. | |||||
| 1116 | 83026 | 36286 | my $comment; | |||
| 1117 | # Most lines have no '#': index() is far cheaper than the regexes. | |||||
| 1118 | 83026 | 48416 | if (index($line, '#') >= 0) { | |||
| 1119 | # Text after the first '#', then again after removing unsafe | |||||
| 1120 | # characters (which can expose new outer whitespace). Both use | |||||
| 1121 | # $TRIMMED_RE, which is linear; see its definition. | |||||
| 1122 | 258 | 462 | my $after = substr $line, index($line, '#') + 1; | |||
| 1123 | 258 | 1229 | ($comment) = $after =~ /\A$TRIMMED_RE/x; | |||
| 1124 | 258 | 203 | if (defined $comment) { | |||
| 1125 | 248 | 2001 | $comment =~ s/$UNSAFE_COMMENT_CHARS_RE//gx; | |||
| 1126 | 248 | 1185 | ($comment) = $comment =~ /\A$TRIMMED_RE/x; | |||
| 1127 | } | |||||
| 1128 | 258 | 238 | $line = substr $line, 0, index($line, '#'); | |||
| 1129 | } | |||||
| 1130 | ||||||
| 1131 | # A line may hold several entries ('A' => 0, 'B' => 0); take them all. | |||||
| 1132 | 83026 | 36301 | my @pairs; | |||
| 1133 | # Opening and closing quotes must match: with ['"]...['"] the key | |||||
| 1134 | # "A'B" was read as 'B" and recorded as a different module, B. | |||||
| 1135 | 83026 | 116860 | while ($line =~ /$PAIR_RE/gx) { | |||
| 1136 | 82539 | 154756 | push @pairs, [ $1 // $2, $3 // $4 // $5 ]; | |||
| 1137 | } | |||||
| 1138 | ||||||
| 1139 | # Defense-in-depth: [^'"]+ already excludes quote characters, but it | |||||
| 1140 | # would accept spaces, ';' and non-ASCII look-alikes. Only a real | |||||
| 1141 | # module name may reach the generated cpanfile. Invalid names are | |||||
| 1142 | # dropped first, so that the line's comment goes to the last entry | |||||
| 1143 | # that is actually kept. | |||||
| 1144 | 83026 82539 | 43935 95640 | @pairs = grep { $_->[0] =~ $MODULE_NAME_RE } @pairs; | |||
| 1145 | ||||||
| 1146 | 83026 | 48757 | for my $i (0 .. $#pairs) { | |||
| 1147 | 82510 82510 | 38011 48850 | my ($mod, $ver) = @{ $pairs[$i] }; | |||
| 1148 | # Anything that is not a version or version range ('.', '_', | |||||
| 1149 | # $VERSION, ...) means "no minimum"; emitting it would produce a | |||||
| 1150 | # cpanfile that CPAN::Meta::Requirements rejects. | |||||
| 1151 | 82510 | 44481 | $ver = 0 unless _valid_requirement($ver); | |||
| 1152 | # First occurrence wins - do not overwrite already-parsed entries. | |||||
| 1153 | # A trailing comment belongs to the entry it follows: the last. | |||||
| 1154 | 82510 | 157120 | $deps->{$phase}{$rel}{$mod} //= { | |||
| 1155 | version => $ver, | |||||
| 1156 | comment => $i == $#pairs ? $comment : undef, | |||||
| 1157 | }; | |||||
| 1158 | } | |||||
| 1159 | } | |||||
| 1160 | ||||||
| 1161 | 81164 | 67447 | return; | |||
| 1162 | } | |||||
| 1163 | ||||||
| 1164 | # _parse_min_perl | |||||
| 1165 | # | |||||
| 1166 | # Purpose: Extract the MIN_PERL_VERSION value from Makefile.PL text. | |||||
| 1167 | # Entry: $_[0] - raw Makefile.PL content string. | |||||
| 1168 | # Exit: The version string (e.g. '5.010'), or undef if not declared. | |||||
| 1169 | sub _parse_min_perl { | |||||
| 1170 | 466 | 3223 | my $content = $_[0]; | |||
| 1171 | 466 | 673 | return undef if !defined $content || ref $content; ## no critic (ProhibitExplicitReturnUndef) | |||
| 1172 | 462 | 2156 | return undef unless $content =~ /\bMIN_PERL_VERSION\b\s*=>\s*$VALUE_TOKEN_RE/x; ## no critic (ProhibitExplicitReturnUndef) | |||
| 1173 | 173 | 219 | my $ver = $1 // $2 // $3; | |||
| 1174 | 173 | 138 | return _valid_version($ver) ? $ver : undef; | |||
| 1175 | } | |||||
| 1176 | ||||||
| 1177 | # _comment_spans | |||||
| 1178 | # | |||||
| 1179 | # Purpose: Find every '#' comment in the content, so that a commented-out | |||||
| 1180 | # one-line dependency hash is not parsed as live. One linear pass; | |||||
| 1181 | # the result is queried with _in_comment for each candidate block. | |||||
| 1182 | # Entry: $_[0] - content string. | |||||
| 1183 | # Exit: ArrayRef of [start, end) offsets, ascending and non-overlapping, | |||||
| 1184 | # each running from an unquoted '#' to the end of its line. | |||||
| 1185 | sub _comment_spans { | |||||
| 1186 | 710 | 1191 | my $content = $_[0]; | |||
| 1187 | 710 | 397 | my @spans; | |||
| 1188 | 710 | 388 | my $offset = 0; | |||
| 1189 | 710 | 12244 | for my $line (split /\n/x, $content, -1) { | |||
| 1190 | # Most lines have no '#' at all; only the rest need the quote-aware | |||||
| 1191 | # scan, which finds the first '#' outside quoted strings (so that | |||||
| 1192 | # e.g. 'C#' is not mistaken for a comment) without building a copy. | |||||
| 1193 | 44975 | 32086 | if (index($line, '#') >= 0 && $line =~ $TO_COMMENT_RE) { | |||
| 1194 | 296 | 430 | push @spans, [ $offset + $+[0] - 1, $offset + length $line ]; | |||
| 1195 | } | |||||
| 1196 | 44975 | 21147 | $offset += length($line) + 1; | |||
| 1197 | } | |||||
| 1198 | 710 | 2056 | return \@spans; | |||
| 1199 | } | |||||
| 1200 | ||||||
| 1201 | # _in_comment | |||||
| 1202 | # | |||||
| 1203 | # Purpose: Decide whether an offset lies inside one of the comment spans. | |||||
| 1204 | # Entry: $_[0] - ArrayRef from _comment_spans; $_[1] - offset. | |||||
| 1205 | # Exit: Boolean. Binary search, so O(log n) per query. | |||||
| 1206 | sub _in_comment { | |||||
| 1207 | 182144 | 92568 | my ($spans, $pos) = @_; | |||
| 1208 | 182144 182144 | 81184 92390 | my ($lo, $hi) = (0, $#{$spans}); | |||
| 1209 | 182144 | 101509 | while ($lo <= $hi) { | |||
| 1210 | 556198 | 281410 | my $mid = int(($lo + $hi) / 2); | |||
| 1211 | 556198 556198 | 238761 290923 | my ($start, $end) = @{ $spans->[$mid] }; | |||
| 1212 | 556198 258765 | 293838 144879 | if ($pos < $start) { $hi = $mid - 1 } | |||
| 1213 | 277045 | 155329 | elsif ($pos >= $end) { $lo = $mid + 1 } | |||
| 1214 | 20388 | 19199 | else { return 1 } | |||
| 1215 | } | |||||
| 1216 | 161756 | 94292 | return 0; | |||
| 1217 | } | |||||
| 1218 | ||||||
| 1219 | # _valid_version | |||||
| 1220 | # | |||||
| 1221 | # Purpose: Decide whether a string is a plausible version number that is | |||||
| 1222 | # safe to write inside a single-quoted cpanfile literal. | |||||
| 1223 | # Entry: $_[0] - candidate version string. | |||||
| 1224 | # Exit: Boolean: an optional leading 'v' followed only by ASCII digits, | |||||
| 1225 | # dots and underscores, with at least one digit. | |||||
| 1226 | sub _valid_version { | |||||
| 1227 | 188 | 3868 | my $ver = $_[0]; | |||
| 1228 | 188 | 765 | return defined $ver && $ver =~ $VERSION_RE ? 1 : 0; | |||
| 1229 | } | |||||
| 1230 | ||||||
| 1231 | # _valid_requirement | |||||
| 1232 | # | |||||
| 1233 | # Purpose: Decide whether a string is a module version requirement that is | |||||
| 1234 | # safe to write inside a single-quoted cpanfile literal: a single | |||||
| 1235 | # version (see _valid_version) or a CPAN::Meta version range. | |||||
| 1236 | # Entry: $_[0] - candidate string. | |||||
| 1237 | # Exit: Boolean. | |||||
| 1238 | sub _valid_requirement { | |||||
| 1239 | 82730 | 39841 | my $req = $_[0]; | |||
| 1240 | 82730 | 131144 | return defined $req && $req =~ $REQUIREMENT_RE ? 1 : 0; | |||
| 1241 | } | |||||
| 1242 | ||||||
| 1243 | # _printable | |||||
| 1244 | # | |||||
| 1245 | # Purpose: Make text from a file or the environment safe to put in an | |||||
| 1246 | # error or warning. SECURITY: such text is printed to the user's | |||||
| 1247 | # terminal, where escape sequences (e.g. ESC ] 0 ; ... to retitle | |||||
| 1248 | # the window, or CR and ESC [ 2 K to erase the line and print | |||||
| 1249 | # something else) could hide or forge messages. | |||||
| 1250 | # Entry: $_[0] - any value (stringified). | |||||
| 1251 | # Exit: The string with control characters and bidirectional-override | |||||
| 1252 | # characters replaced by \x{..} escapes; everything else as is. | |||||
| 1253 | sub _printable { | |||||
| 1254 | 490 | 390 | my $text = "$_[0]"; | |||
| 1255 | 490 14 | 1182 21 | $text =~ s/($UNSAFE_COMMENT_CHARS_RE)/sprintf '\\x{%X}', ord $1/gex; | |||
| 1256 | 490 | 1336 | return $text; | |||
| 1257 | } | |||||
| 1258 | ||||||
| 1259 | # _load_develop_config | |||||
| 1260 | # | |||||
| 1261 | # Return the develop-tools hash from the user's YAML config file, | |||||
| 1262 | # or %DEFAULT_DEVELOP when no config file exists. | |||||
| 1263 | # Entry: None - reads from the filesystem at a well-known path. | |||||
| 1264 | # Exit: HashRef { Module::Name => minimum_version_or_0 }. | |||||
| 1265 | # Effects: Reads from disk. Croaks on YAML parse failure. Carps when the | |||||
| 1266 | # config file lacks a 'develop' key. | |||||
| 1267 | sub _load_develop_config { | |||||
| 1268 | # Path::Tiny and YAML::Tiny use eval internally, which resets $@; keep the | |||||
| 1269 | # caller's value intact so an enclosing eval/$@ check is not disturbed. | |||||
| 1270 | 273 | 39517 | local $@ = q{}; | |||
| 1271 | ||||||
| 1272 | # Guard: no usable home directory (containers, chroots, CI). length() | |||||
| 1273 | # of undef is undef, so one test rejects both undef and ''. | |||||
| 1274 | 273 | 451 | my $home = File::HomeDir->my_home; | |||
| 1275 | 273 | 824 | return {%DEFAULT_DEVELOP} unless length($home // q{}); | |||
| 1276 | ||||||
| 1277 | 264 | 255 | my $cfg_path = Path::Tiny::path($home)->child('.config', 'makefilepl2cpanfile.yml'); | |||
| 1278 | 264 | 6203 | my $cfg_shown = _printable($cfg_path); | |||
| 1279 | ||||||
| 1280 | # Guard: the path cannot be examined. A missing file means "use the | |||||
| 1281 | # defaults"; any other stat failure (EACCES, ELOOP, stale NFS...) must not | |||||
| 1282 | # silently change the output. | |||||
| 1283 | 264 | 199 | my @stat = stat "$cfg_path"; | |||
| 1284 | 264 | 1325 | unless (@stat) { | |||
| 1285 | 78 1 1 1 | 163 156 441 3 | Carp::croak "Failed to parse $cfg_shown: $!" unless $!{ENOENT} || $!{ENOTDIR}; | |||
| 1286 | 75 | 787 | return {%DEFAULT_DEVELOP}; | |||
| 1287 | } | |||||
| 1288 | ||||||
| 1289 | # Guard: not a regular file. A FIFO would block the read forever and a | |||||
| 1290 | # device could stream without end. The mode bits come from @stat, not | |||||
| 1291 | # from Perl's implicit '_' buffer, which another stat could replace. | |||||
| 1292 | 186 | 258 | return {%DEFAULT_DEVELOP} unless Fcntl::S_ISREG($stat[2]); | |||
| 1293 | ||||||
| 1294 | # Guard: unreadable or invalid YAML. YAML::Tiny reports failure by dying | |||||
| 1295 | # (current versions) or by returning false with errstr set (older ones); | |||||
| 1296 | # both become the single documented message, without YAML::Tiny's own | |||||
| 1297 | # location. | |||||
| 1298 | 179 179 | 115 131 | my $yaml = eval { YAML::Tiny->read("$cfg_path") }; | |||
| 1299 | 179 | 22291 | unless ($yaml) { | |||
| 1300 | 44 | 68 | my $err = $@ || YAML::Tiny->errstr() // q{}; | |||
| 1301 | 44 | 123 | $err =~ s/[ ]at[ ]\S++[ ]line[ ]\d++\.?\n?\z//x; | |||
| 1302 | 44 | 47 | Carp::croak "Failed to parse $cfg_shown: " . _printable($err); | |||
| 1303 | } | |||||
| 1304 | ||||||
| 1305 | # Guard: no develop hash. Checking the document's type first means a | |||||
| 1306 | # document that is not a hash is never dereferenced. | |||||
| 1307 | 135 | 98 | my $doc = $yaml->[0]; | |||
| 1308 | 135 | 151 | my $develop = ref $doc eq 'HASH' ? $doc->{develop} : undef; | |||
| 1309 | 135 | 123 | unless (ref $develop eq 'HASH') { | |||
| 1310 | 41 | 159 | Carp::carp "No 'develop' key found in $cfg_shown; using defaults"; | |||
| 1311 | 41 | 2825 | return {%DEFAULT_DEVELOP}; | |||
| 1312 | } | |||||
| 1313 | ||||||
| 1314 | # SECURITY: validate every key (module name) and value (version) before | |||||
| 1315 | # use. Keys are arbitrary strings: without this guard a key such as | |||||
| 1316 | # "Safe'; system('evil'); requires 'X" would close the quoted literal in | |||||
| 1317 | # _fmt_dep and inject code into the generated cpanfile, which cpanm runs. | |||||
| 1318 | 94 | 60 | my %clean; | |||
| 1319 | 94 94 | 70 123 | for my $mod (keys %{$develop}) { | |||
| 1320 | 130 | 309 | unless ($mod =~ $MODULE_NAME_RE) { | |||
| 1321 | 10 | 14 | Carp::carp "Skipping invalid module name in $cfg_shown: '" . _printable($mod) . q{'}; | |||
| 1322 | 10 | 771 | next; | |||
| 1323 | } | |||||
| 1324 | # Premise 1: a null value means "any version", i.e. 0. | |||||
| 1325 | # Premise 2: _valid_requirement accepts '0' (it contains a digit). | |||||
| 1326 | # Conclusion: '' is the only extra value that needs allowing. | |||||
| 1327 | 120 | 134 | my $v = $develop->{$mod} // 0; | |||
| 1328 | 120 | 138 | unless ($v eq q{} || _valid_requirement($v)) { | |||
| 1329 | 23 | 31 | Carp::carp "Skipping invalid version for '$mod' in $cfg_shown: '" . _printable($v) . q{'}; | |||
| 1330 | 23 | 1215 | $v = 0; | |||
| 1331 | } | |||||
| 1332 | 120 | 125 | $clean{$mod} = $v; | |||
| 1333 | } | |||||
| 1334 | 94 | 211 | return \%clean; | |||
| 1335 | } | |||||
| 1336 | ||||||
| 1337 | # _emit | |||||
| 1338 | # | |||||
| 1339 | # Purpose: Pure formatter - converts the structured dependency hash and an | |||||
| 1340 | # optional minimum Perl version into a valid cpanfile string. | |||||
| 1341 | # Entry: $_[0] - HashRef (see DATA STRUCTURE section in POD) | |||||
| 1342 | # $_[1] - optional Str minimum Perl version (e.g. '5.010') | |||||
| 1343 | # Exit: Scalar string; always terminated with exactly one newline. | |||||
| 1344 | # Never returns undef. | |||||
| 1345 | # | |||||
| 1346 | # Runtime deps are emitted at the top level (no 'on' block) per cpanfile | |||||
| 1347 | # convention. All other phases get 'on phase => sub { ... }' blocks. | |||||
| 1348 | # Within each phase, relationships are emitted in @REL_ORDER order; | |||||
| 1349 | # modules within each relationship are sorted alphabetically. | |||||
| 1350 | # Inline comments are re-emitted after the semicolon on the same line. | |||||
| 1351 | # A version of 0 or '' means "any version" and is omitted. | |||||
| 1352 | sub _emit { | |||||
| 1353 | 432 | 17103 | my ($deps, $min_perl) = @_; | |||
| 1354 | ||||||
| 1355 | # Build the output as a list of sections joined by blank lines. | |||||
| 1356 | 432 | 318 | my @sections = ($HEADER); | |||
| 1357 | 432 | 359 | push @sections, "requires 'perl', '$min_perl';" if _has_version($min_perl); | |||
| 1358 | ||||||
| 1359 | # Runtime is emitted at the top level; every other phase in an 'on' | |||||
| 1360 | # block, in @PHASE_ORDER. Both use the same line builder. | |||||
| 1361 | 432 | 488 | for my $phase ('runtime', @PHASE_ORDER) { | |||
| 1362 | 2160 | 2143 | my $indent = $phase eq 'runtime' ? q{} : "\t"; | |||
| 1363 | 2160 4419 4419 2160 | 3268 2030 2433 1506 | my $body = join q{}, map { _fmt_dep(@{$_}, $indent) } @{ _phase_entries($deps->{$phase}) }; | |||
| 1364 | # Premise: an absent phase, or one whose relationships are all | |||||
| 1365 | # empty, yields no entries. Conclusion: it yields no section. | |||||
| 1366 | 2160 | 2148 | next if $body eq q{}; | |||
| 1367 | 748 | 1147 | push @sections, $phase eq 'runtime' | |||
| 1368 | ? $body =~ s/\n\z//rx # the blank-line separator is added by join | |||||
| 1369 | : "on '$phase' => sub {\n$body};"; | |||||
| 1370 | } | |||||
| 1371 | ||||||
| 1372 | 432 | 2063 | return join("\n\n", @sections) . "\n"; | |||
| 1373 | } | |||||
| 1374 | ||||||
| 1375 | # _phase_entries | |||||
| 1376 | # | |||||
| 1377 | # Purpose: List one phase's entries in output order: relationships in | |||||
| 1378 | # @REL_ORDER, modules alphabetically within each. | |||||
| 1379 | # Entry: $_[0] - the phase's hashref ({ rel => { module => entry } }), | |||||
| 1380 | # or undef when the phase is absent. | |||||
| 1381 | # Exit: ArrayRef of [ rel, module, entry ] triples; empty when none. | |||||
| 1382 | sub _phase_entries { | |||||
| 1383 | 2165 | 2627 | my $phase = $_[0] || {}; | |||
| 1384 | 2165 | 1080 | my @entries; | |||
| 1385 | 2165 | 1496 | for my $rel (@REL_ORDER) { | |||
| 1386 | 8660 | 12996 | my $mods = $phase->{$rel} || {}; | |||
| 1387 | 8660 8660 | 16195 9466 | push @entries, [ $rel, $_, $mods->{$_} ] for sort keys %{$mods}; | |||
| 1388 | } | |||||
| 1389 | 2165 | 3408 | return \@entries; | |||
| 1390 | } | |||||
| 1391 | ||||||
| 1392 | # _fmt_dep | |||||
| 1393 | # | |||||
| 1394 | # Purpose: Format a single dependency line for cpanfile output, including | |||||
| 1395 | # the optional version constraint and inline comment. | |||||
| 1396 | # Entry: $_[0] - relationship keyword (e.g. 'requires', 'recommends'). | |||||
| 1397 | # $_[1] - module name string. | |||||
| 1398 | # $_[2] - entry hashref { version => ..., comment => ... }. | |||||
| 1399 | # $_[3] - indentation prefix ('' for runtime, "\t" for phase blocks). | |||||
| 1400 | # Exit: A complete formatted line, including trailing newline. | |||||
| 1401 | sub _fmt_dep { | |||||
| 1402 | 4437 | 6580 | my ($rel, $mod, $entry, $indent) = @_; | |||
| 1403 | ||||||
| 1404 | 4437 | 2325 | my $line = "${indent}$rel '$mod'"; | |||
| 1405 | 4437 | 2368 | $line .= ", '$entry->{version}'" if _has_version($entry->{version}); | |||
| 1406 | ||||||
| 1407 | # Premise 1: every producer of entries stores a comment that is undef | |||||
| 1408 | # or non-empty (_extract_pairs turns an empty comment into undef; merged | |||||
| 1409 | # and configured entries have none). Premise 2: _fmt_dep is only called | |||||
| 1410 | # by _emit on such entries. Conclusion: defined() is sufficient. | |||||
| 1411 | 4437 | 2421 | if (defined $entry->{comment}) { | |||
| 1412 | 145 | 108 | $line .= "; # $entry->{comment}\n"; | |||
| 1413 | } else { | |||||
| 1414 | 4292 | 2072 | $line .= ";\n"; | |||
| 1415 | } | |||||
| 1416 | ||||||
| 1417 | 4437 | 3334 | return $line; | |||
| 1418 | } | |||||
| 1419 | ||||||
| 1420 | # _has_version | |||||
| 1421 | # | |||||
| 1422 | # Decide whether a version value represents a real minimum version | |||||
| 1423 | # constraint that should be written into the cpanfile output. | |||||
| 1424 | # Entry: $_[0] - version value (scalar, possibly undef or numeric '0'). | |||||
| 1425 | # Exit: Boolean: true if the version should be emitted; false if it | |||||
| 1426 | # means "any version" (undef, empty string, or numeric zero). | |||||
| 1427 | sub _has_version { | |||||
| 1428 | 4911 | 69042 | my $ver = $_[0]; | |||
| 1429 | ||||||
| 1430 | # Premise 1: every version that reaches here is undef, 0, '' or a string | |||||
| 1431 | # accepted by _valid_requirement: a single version (ASCII digits, '.', | |||||
| 1432 | # '_', optional 'v') or a range, which always contains an operator. | |||||
| 1433 | # Premise 2: a single version is zero exactly when none of its digits is | |||||
| 1434 | # 1-9 ('0', '0.000', 'v0.0.0', '0_0'); a range means "any version" only | |||||
| 1435 | # when it is '>=' a zero version. | |||||
| 1436 | # Conclusion: two tests decide it. | |||||
| 1437 | 4911 | 2869 | return 0 unless defined $ver; | |||
| 1438 | 4610 | 3020 | return $ver =~ /\A \s* >= \s* v? [0._]* \s* \z/x ? 0 : 1 if $ver =~ /[<>=!]/x; | |||
| 1439 | 4605 | 3454 | return $ver =~ /[1-9]/x ? 1 : 0; | |||
| 1440 | } | |||||
| 1441 | ||||||
| 1442 | ||||||
| 1443 | # ----------------------------------------------------------------------- | |||||
| 1444 | # Post-release roadmap (from the 0.05 gap analysis) | |||||
| 1445 | # ----------------------------------------------------------------------- | |||||
| 1446 | # | |||||
| 1447 | # Features | |||||
| 1448 | # TODO: Write CPAN::Meta optional_features as cpanfile 'feature' blocks, | |||||
| 1449 | # and carry x_* custom phases through instead of dropping them. | |||||
| 1450 | # TODO: Command line: --diff should exit non-zero when the cpanfile would | |||||
| 1451 | # change (like 'git diff --exit-code'); add --makefile FILE, | |||||
| 1452 | # --output FILE, --quiet, and '-' to read Makefile.PL from stdin. | |||||
| 1453 | # TODO: Read other build files: Build.PL (Module::Build requires, | |||||
| 1454 | # build_requires, test_requires, configure_requires, recommends) and | |||||
| 1455 | # possibly dist.ini; consider the reverse direction (cpanfile to a | |||||
| 1456 | # PREREQ_PM snippet). | |||||
| 1457 | # TODO: Keep more of a hand-written cpanfile than its develop block: | |||||
| 1458 | # feature blocks, extra 'on test' entries, and comments inside the | |||||
| 1459 | # develop block. | |||||
| 1460 | # TODO: Optional, never default, mode that runs Makefile.PL in an isolated | |||||
| 1461 | # sandbox to capture dependencies computed by code. | |||||
| 1462 | # TODO: Translatable messages (e.g. Locale::TextDomain); all messages are | |||||
| 1463 | # English only. | |||||
| 1464 | # | |||||
| 1465 | # Technical debt | |||||
| 1466 | # TODO: Replace the brace matching with PPI: parses Perl without running | |||||
| 1467 | # it, understands quoting, comments and heredocs (so a brace inside | |||||
| 1468 | # a string would no longer count), and could flag conditional | |||||
| 1469 | # dependencies instead of making them unconditional. | |||||
| 1470 | # TODO: Move bin/makefilepl2cpanfile's logic into a module | |||||
| 1471 | # (e.g. App::makefilepl2cpanfile::CLI->run(\@ARGV) returning an exit | |||||
| 1472 | # code) for in-process tests, real coverage, and fewer subprocess | |||||
| 1473 | # tests (slow, and fragile on Windows). | |||||
| 1474 | # TODO: Consolidate the test suite: move duplicated helpers (empty_home, | |||||
| 1475 | # make_mf, use_home, run_cli) into t/lib, and merge the overlap | |||||
| 1476 | # between function.t, unit.t and extended_tests.t. | |||||
| 1477 | # TODO: Readonly hashes and arrays (%PHASE_MAP, %VALID_PHASE, %VALID_REL, | |||||
| 1478 | # %LEGACY_KEY, @REL_ORDER, @PHASE_ORDER, %DEFAULT_DEVELOP) are tied, | |||||
| 1479 | # adding overhead to every access; use plain lexicals or constant | |||||
| 1480 | # lists. | |||||
| 1481 | # TODO: In the command-line tool, rename the temporary file directly | |||||
| 1482 | # instead of Path::Tiny's move (File::Copy::move moves INTO a | |||||
| 1483 | # directory; only the non-regular-file guard prevents that today), | |||||
| 1484 | # and close the small race between the symlink check and the write. | |||||
| 1485 | # TODO: Consider a size limit for Makefile.PL and the existing cpanfile, so | |||||
| 1486 | # a huge hostile file cannot exhaust memory. | |||||
| 1487 | # TODO: CI: Devel::Cover for the command-line tool's subprocesses (via | |||||
| 1488 | # PERL5OPT). | |||||
| 1489 | ||||||
| 1490 | 1; | |||||
| 1491 | ||||||