File Coverage

File:blib/lib/App/makefilepl2cpanfile.pm
Coverage:98.9%

linestmtbrancondsubtimecode
1package 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
26our $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.
34Readonly::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.
43Readonly::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.
51Readonly::Hash my %VALID_PHASE => map { $_ => 1 }
52        qw(runtime configure build test develop);
53
54# Valid cpanfile relationship keywords inside each phase block.
55Readonly::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).
60Readonly::Array my @PHASE_ORDER => qw(configure build test develop);
61
62# Within each phase block, emit relationships in this order.
63Readonly::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.
67Readonly::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.
77Readonly::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.
82Readonly::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'.
92Readonly::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.
104Readonly::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.
116Readonly::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.
129Readonly::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.
144Readonly::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.
156Readonly::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.
162Readonly::Scalar my $SIMPLE_OPEN_RE  => qr/ \b ($SIMPLE_KEY_RE) \s*+ => \s*+ \{ /x;
163Readonly::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.
165Readonly::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.
167Readonly::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.)
177Readonly::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.
183Readonly::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.
189Readonly::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.
202Readonly::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
656sub 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
789sub 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
955sub 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.
1042sub _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.
1074sub _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.
1107sub _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.
1169sub _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.
1185sub _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.
1206sub _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.
1226sub _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.
1238sub _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.
1253sub _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.
1267sub _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.
1352sub _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.
1382sub _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.
1401sub _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).
1427sub _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
14901;
1491