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:
- Runtime dependencies (
PREREQ_PM) - Build, test, and configure requirements (
BUILD_REQUIRES,TEST_REQUIRES,CONFIGURE_REQUIRES) - Structured
prereqs => { phase => { rel => { ... } } }blocks (CPAN Meta Spec format), includingrecommendsandsuggestsrelationships - Inline comments attached to dependency entries
- Optional author/development dependencies in a
developblock
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
- Because parsing is regex-based and the
Makefile.PLis nevereval'd, dynamically generated dependency lists (e.g. those produced byif/unlessbranches or subroutine calls) cannot be detected. - Encapsulation enforcement (
Sub::Privateinenforcemode) is not applied; the_prefix convention is used instead. A future release may addSub::Privateonce itsenforce-mode API is verified.
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.