Added experimental module dependency resolver
parent
a18fd401c5
commit
3040ee0f3c
@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
use warnings;
|
||||||
|
use 5.026;
|
||||||
|
use Getopt::Long qw/ HelpMessage :config posix_default gnu_compat
|
||||||
|
bundling auto_version auto_help /;
|
||||||
|
use Graph ();
|
||||||
|
use Memoize 'memoize';
|
||||||
|
use Memoize::Storable ();
|
||||||
|
|
||||||
|
=head1 SYNOPSIS
|
||||||
|
|
||||||
|
depend.pl MODULE(s)
|
||||||
|
OPTIONS:
|
||||||
|
-v | --verbose - more output
|
||||||
|
-t | --want-test - include modules needed for test phase
|
||||||
|
-p | --perl-ver VER - Perl version for corelist (default: 5.026)
|
||||||
|
-c | --cache-file FILE - cache file for MetaCPAN API requests
|
||||||
|
(default: /tmp/.metacpan_deps_cache)
|
||||||
|
-C | --clear-cache - clear cache before running
|
||||||
|
|
||||||
|
=head1 DESCRIPTION
|
||||||
|
|
||||||
|
A test of resolving module dependences, currently via the MetaCPAN API.
|
||||||
|
(The list of dependencies that MetaCPAN knows about may not always be complete.)
|
||||||
|
|
||||||
|
Outputs a possible install order that should satisfy dependencies.
|
||||||
|
Note this order can change across runs, but theoretically it should
|
||||||
|
always be a valid install order.
|
||||||
|
|
||||||
|
Notes for WebPerl:
|
||||||
|
Could be used in F<build.pl>.
|
||||||
|
I don't really need C<is_installed>.
|
||||||
|
Perhaps instead of C<is_core> I should check if the module exists
|
||||||
|
in the Perl source tree and is enabled in F<config.sh>...
|
||||||
|
|
||||||
|
=cut
|
||||||
|
|
||||||
|
our $VERSION = '0.01-beta';
|
||||||
|
|
||||||
|
GetOptions(
|
||||||
|
'v|verbose' => \(my $VERBOSE),
|
||||||
|
't|want-test' => \(my $WANT_TEST),
|
||||||
|
'p|perl-ver=s' => \(my $PERL_VER='5.026'),
|
||||||
|
'c|cache-file=s' => \(my $CACHE_FILE='/tmp/.metacpan_deps_cache'),
|
||||||
|
'C|clear-cache' => \(my $NO_CACHE),
|
||||||
|
) or HelpMessage(-exitval=>255);
|
||||||
|
HelpMessage(-msg=>'Not enough arguments',-exitval=>255) unless @ARGV;
|
||||||
|
|
||||||
|
|
||||||
|
if ($NO_CACHE && -e $CACHE_FILE)
|
||||||
|
{ unlink($CACHE_FILE)==1 or die "Failed to unlink $CACHE_FILE: $!" }
|
||||||
|
tie my %get_deps_cache, 'Memoize::Storable', $CACHE_FILE;
|
||||||
|
memoize 'get_deps', SCALAR_CACHE=>[HASH=>\%get_deps_cache], LIST_CACHE=>'FAULT';
|
||||||
|
memoize 'is_core';
|
||||||
|
memoize 'is_installed';
|
||||||
|
|
||||||
|
|
||||||
|
my $dep_graph = Graph->new(directed => 1);
|
||||||
|
resolve_deps($_, $dep_graph) for @ARGV;
|
||||||
|
my @topo = $dep_graph->topological_sort;
|
||||||
|
say for reverse @topo;
|
||||||
|
warn "No (non-core) dependencies\n" unless @topo;
|
||||||
|
|
||||||
|
|
||||||
|
use MetaCPAN::Client ();
|
||||||
|
sub get_deps { # will be memoized (and persisted)
|
||||||
|
my ($module) = @_;
|
||||||
|
state $mcpan = MetaCPAN::Client->new();
|
||||||
|
$VERBOSE and say STDERR "Fetching dependencies of $module from MetaCPAN API";
|
||||||
|
return $mcpan->release($mcpan->module($module)->distribution)->dependency;
|
||||||
|
}
|
||||||
|
|
||||||
|
use Module::CoreList ();
|
||||||
|
sub is_core { # will be memoized
|
||||||
|
my ($module,$version) = @_;
|
||||||
|
return Module::CoreList::is_core($module,$version,$PERL_VER);
|
||||||
|
}
|
||||||
|
|
||||||
|
use Module::Load::Conditional ();
|
||||||
|
sub is_installed { # will be memoized
|
||||||
|
my ($module,$version) = @_;
|
||||||
|
return Module::Load::Conditional::check_install(module=>$module,version=>$version);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub resolve_deps {
|
||||||
|
my $module = shift;
|
||||||
|
my $graph = @_ ? shift : Graph->new(directed => 1);
|
||||||
|
for my $dep ( get_deps($module)->@* ) {
|
||||||
|
next if is_core( $dep->{module}, $dep->{version} ); # ignore core modules
|
||||||
|
next if $dep->{module} eq 'perl'; # ignore perl dist itself
|
||||||
|
next unless $dep->{relationship} eq 'requires'; # ignore 'recommends' and 'suggests'
|
||||||
|
die "Unknown relationship '$dep->{relationship}'"
|
||||||
|
unless $dep->{relationship}=~/\A(?:requires|recommends|suggests)\z/;
|
||||||
|
next if $dep->{phase} eq 'develop'; # ignore phase 'develop'
|
||||||
|
next if !$WANT_TEST && $dep->{phase} eq 'test'; # ignore phase 'test' unless user wants it
|
||||||
|
next if $dep->{phase}=~/\Ax_/; # ignore e.g. "x_Dist_Zilla"
|
||||||
|
die "Unknown phase '$dep->{phase}'"
|
||||||
|
unless $dep->{phase}=~/\A(?:configure|build|runtime|test)\z/;
|
||||||
|
my $installed = is_installed( $dep->{module}, $dep->{version} ); # just for info
|
||||||
|
$VERBOSE and say STDERR "$module requires $dep->{module}",
|
||||||
|
$dep->{version} ? " (version $dep->{version})" : " (any version)",
|
||||||
|
" for $dep->{phase}",
|
||||||
|
$installed ? " (installed)" : " (not installed)";
|
||||||
|
$graph->add_edge($module, $dep->{module});
|
||||||
|
die "Fatal: Circular dependency detected (just added $module->$dep->{module})"
|
||||||
|
if $graph->has_a_cycle;
|
||||||
|
resolve_deps($dep->{module}, $graph)
|
||||||
|
}
|
||||||
|
return $graph;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue