NAME

App::makefilepl2cpanfile - Convert Makefile.PL to a cpanfile automatically

VERSION

SYNOPSIS

    use App::makefilepl2cpanfile;

    my $cpanfile_text = App::makefilepl2cpanfile::generate(
            makefile     => 'Makefile.PL',
            existing     => '',   # optional: existing cpanfile text to merge
            with_develop => 1,    # include author/developer dependencies
    );

    path('cpanfile')->spew_utf8($cpanfile_text);

DESCRIPTION

Parses a Makefile.PL file without evaluating it and produces a cpanfile string containing:

CONFIGURATION

An optional YAML file at ~/.config/makefilepl2cpanfile.yml overrides the default develop-phase tools:

    develop:
      Perl::Critic: 0
      Devel::Cover: 0
      My::Extra::Tool: '1.00'

DATA STRUCTURE

parse_prereqs() and the internal pipeline use a three-level hashref:

    {
      phase_name => {
        relationship => {
          'Module::Name' => { version => '1.0', comment => 'why it is needed' },
        },
      },
    }

phase_name ∈ { runtime, configure, build, test, develop }. relationship ∈ { requires, recommends, suggests }. version is 0 when no minimum is declared. comment is undef when no inline comment was present.

METHODS

generate(%args)

Parses a Makefile.PL and returns a complete cpanfile string.

PSEUDOCODE

    1. Validate and normalise arguments; croak if makefile is unreadable.
    2. Slurp makefile content; extract MIN_PERL_VERSION.
    3. Call parse_prereqs() to build the phase/rel/module/entry structure.
    4. If an existing cpanfile string was supplied, merge its 'develop'
       block (all relationships) without overwriting freshly-parsed entries.
    5. If with_develop: load user config (or built-in defaults) and inject
       missing 'requires' develop tools — never overwrite explicit entries.
    6. Delegate to _emit() and return the formatted string.

API SPECIFICATION

    Arguments (named hash or single hashref):
      makefile     Str   Path to Makefile.PL.  Default: 'Makefile.PL'
      existing     Str   Existing cpanfile text to merge.  Default: ''
      with_develop Bool  Inject default dev tools.  Default: 1 (true)

    Returns: Str — complete cpanfile text, terminated with a single newline.

EXAMPLE

    # Minimal usage — generate from the project's own Makefile.PL
    my $out = App::makefilepl2cpanfile::generate();
    path('cpanfile')->spew_utf8($out);

    # Preserve hand-curated develop entries from an existing cpanfile
    my $out = App::makefilepl2cpanfile::generate(
            makefile => 'dist/Makefile.PL',
            existing => path('cpanfile')->slurp_utf8,
    );

MESSAGES

    "Cannot read '$makefile'"
        The supplied path does not exist, is a directory, or is not readable.
        Resolution: verify the path and filesystem permissions.

    "Failed to parse $cfg_file: ..."
        The user config file exists but contains invalid YAML.
        Resolution: validate the YAML syntax; or delete the file to use defaults.

    "No 'develop' key found in $cfg_file; using defaults"
        The config file exists but lacks a 'develop' section.
        Resolution: add a develop: block, or delete the file to use defaults.

parse_prereqs($content)

Extracts all dependency declarations from a Makefile.PL string and returns them structured by cpanfile phase and relationship type. Exposed as a public function so callers (e.g. bin/makefilepl2cpanfile --check) can reuse the parsing logic without duplicating regexes.

Both the simple PREREQ_PM => { ... } form and the structured prereqs => { phase => { rel => { ... } } } form (including those nested under META_MERGE) are parsed. Inline comments attached to module entries are captured and preserved for round-trip fidelity.

API SPECIFICATION

    Arguments:
      $content   Str   Raw text of a Makefile.PL

    Returns: HashRef (see L</DATA STRUCTURE>)
      {
        phase => {
          rel => {
            'Module::Name' => { version => version_str, comment => str_or_undef },
          },
        },
      }
      Absent phases/relationships are not present in the hashref.
      version is 0 when no minimum is declared.
      comment is undef when no inline comment was present.

EXAMPLE

    my $deps = App::makefilepl2cpanfile::parse_prereqs(
        path('Makefile.PL')->slurp_utf8
    );

    # Iterate over all phases and relationships
    for my $phase (sort keys %{$deps}) {
        for my $rel (sort keys %{ $deps->{$phase} }) {
            for my $mod (sort keys %{ $deps->{$phase}{$rel} }) {
                my $e = $deps->{$phase}{$rel}{$mod};
                printf "%s %s %s => %s\n", $phase, $rel, $mod, $e->{version};
            }
        }
    }

MESSAGES

    No errors or warnings — unrecognised content is silently ignored.

LIMITATIONS

SUPPORT

This module is provided as-is without any warranty.

Bugs and feature requests: https://github.com/nigelhorne/App-makefilepl2cpanfile/issues

AUTHOR

Nigel Horne njh@nigelhorne.com

FORMAL SPECIFICATION

generate

    -- generate maps named arguments to a cpanfile string
    generate : Args → Str
    where
      Args ≙ [makefile : Path; existing : Str; with_develop : 𝔹]

    generate(a) ≙
      let content ≙ slurp(a.makefile)
          deps    ≙ parse_prereqs(content)
          merged  ≙ deps ⊕ {develop ↦
                       deps.develop ∪ extract_develop(a.existing)}
          final   ≙ if a.with_develop
                    then merged ⊕ {develop ↦
                           {requires ↦ load_config() ▷ merged.develop.requires}}
                    else merged
      in _emit(final, min_perl(content))
    -- (▷) right-biases toward the right operand: existing entries win.

parse_prereqs

    parse_prereqs : Str → DepMap
    where
      DepMap    ≙ Phase ↦ (Rel ↦ (ModName ↦ Entry))
      Entry     ≙ [version : VersionStr; comment : Str ∪ {⊥}]
      Phase     ∈ {runtime, configure, build, test, develop}
      Rel       ∈ {requires, recommends, suggests}

    parse_prereqs(s) ≙
      simple_deps(s) ⊕ structured_deps(s)
    where
      simple_deps(s)     ≙ ⋃ { extract_simple(k, s) | k ∈ dom(PHASE_MAP) }
      structured_deps(s) ≙ ⋃ { extract_prereqs_block(b) | b ∈ prereqs_blocks(s) }

LICENCE AND COPYRIGHT

Copyright 2025-2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.