Category:
Author/Contact Info theorbtwo
Description:

This will create a graph (on STDOUT, in PNG format) of the dependencies of a given module (which must be on CPAN).

It depends on GraphViz, LWP, and YAML. (It also depends on having a working CPAN.pm.)

Note the blacklist near the top, of modules that it cannot figure out the dependencies for. It will get the dependencies from the META.yml file, if there is one, or the Makefile.PL, if there is not. In no case does it attempt to download the entire distro; it will only download the META.yml or Makefile.PL file. (As well as whatever CPAN.pm needs to do it's job.)

Currently, the only known module that it misgraphs is Graph -- GraphViz seems to have a bug dealing with nodes named Graph. It "almost" works for Graph, as well -- it's missing the arrow between Graph and Heap, but does show both modules.

Note that it depends on CPAN.pm to choose how to satisify a request for a module. This means that it will often not pick the most "reasonable" way to get a module, including if that module is already installed on the system you are running it on. CPAN.pm always tries to get the version of a module uploaded most recently (I think), rather then using the one included with perl (if there is one), or the version in the like-named package.

Oh, a quick note on the output: Boxes label distributions, circles label modules. Red circles are for blacklisted distributions (which we pretend have no dependencies!)

This is written to be easy to convert to a CGI, but doing so would yeild a poor UI -- it is slow, because it needs to run Makefile.PLs in many cases, and download files. (Please, module authors: if your Makefile.PL is non-trival, include a META.yml file!)

#!/usr/bin/perl
use warnings;
use strict;
# $|=1;

$ENV{PERL_MM_USE_DEFAULT}++;
use ExtUtils::MakeMaker;
use GraphViz ();
use CPAN ();
use LWP::Simple qw(get);
# use DDS qw(Dump Dumper);
use Data::Dumper qw(Dumper);
use YAML ();

# Ugh.  Semi-global.
my %makemaker_info;
{
  no warnings 'redefine';
  sub ExtUtils::MakeMaker::WriteMakefile {
    %makemaker_info = @_;
  }
}

my @workqueue = @ARGV;
my %done;
my %blacklist = (# No metadata source
                 'ABERGMAN/ponie-2'=>'', 
                 # Requires it's Changes file
                 'GAAS/libwww-perl-5.79'=>'', 
                 # system('make'), which I can't figure out how to kee
+p from getting to STDOUT
                 'GBARR/Scalar-List-Utils-1.13'=>'' 
                );

sub print_sts {
  print STDERR @_;
}

my $graph = GraphViz->new;

# Force CPAN to load it's stuff now, so we don't have to worry
# about rejigging STDOUT every time.
{
  local *STDOUT;
  *STDOUT=*STDERR{IO};
  CPAN::expand('Module', 'this_does_not_exist');
}

while (@workqueue) {
  my $name = shift @workqueue;
  next if ($done{$name});
  
  print_sts "Working on $name\n";
  
  my $module = CPAN::expand("Module", $name);
  if (!$module) {
    warn "Couldn't find $name on CPAN!\n";
    next;
  }
  
  print_sts Dumper($module);
  
  
  my @depends = eval {get_dependlist($module)};
  

  if ($@) {
    $graph->add_node($name, cluster => distro_name($module), color => 
+'red');
  } else {
    $graph->add_node($name, cluster => distro_name($module));
    foreach my $submod (@depends) {
      print_sts "$name depends on $submod\n";
      push @workqueue, $submod;
      $graph->add_edge($name, $submod);
    }
  }
  
  
  $done{$name}++;
}

print_sts "Rendering\n";

print $graph->as_png;

sub get_dependlist {
  my $module=shift;
  
  my $sco_path = sco_path($module);
  
  if (!$sco_path) {
    print_sts "Skipping.\n";
    die "Blacklisted";
  }
  
  print_sts "... at sco path $sco_path\n";
  
  my $metayaml = get("$sco_path/META.yml");
  
  if ($metayaml) {
    my $meta = YAML::Load($metayaml);
    
    # Some META.ymls seem to give requires as empty-string.
    # for example, Class-Factory-Util.
    return unless ref($meta->{requires}) eq 'HASH';
    
    return (keys %{$meta->{requires}});
  }
  
  my $makefilepl = get("$sco_path/Makefile.PL");

  if ($makefilepl) {
    {
      # Keep the Makefile.PL from trashing our standard out (which sho
+uld be a png).
      local *STDOUT;
      *STDOUT=*STDERR{IO};
      local *STDIN;
      *STDIN=*STDERR{IO};
      no warnings; no strict;
      eval $makefilepl;
    }
    
    if ($@) {
      die "Couldn't eval Makefile.PL: $@";
    }
    
    return keys %{$makemaker_info{PREREQ_PM}};
  }

  
  die "Couldn't find any way to get metadata for $sco_path.\n";
}

# Given a CPAN::Module object, return the base URI to the extracted ve
+rsion
# on search.cpan.org
sub sco_path {
  my $mod = shift;
  
  # FIXME: Is there a better way to get this?
  # NOTE: Test with "perl"; it has userid P5P, but is in the directory
+ of the
  # current stable pumpking
  my $cpanf = $mod->cpan_file;
  unless ($cpanf =~ m|/.*/([^/]+/.*).tar.gz$|) {
    die "Can't find distro name from $cpanf.\n";
  }
  
  if (exists $blacklist{$1}) {
    return '';
  }
  
  return "http://search.cpan.org/src/" . $1;
}

# Given a CPAN::Module object, return the name of the distribution
sub distro_name {
  my $mod = shift;
  
  local $_ = $mod->cpan_file;
  m|.*/(.*)-.*\.tar\.gz$|;

  return $1;
}
Replies are listed 'Best First'.
Re: Module Dependency Grapher
by ysth (Canon) on May 07, 2004 at 15:14 UTC
    (Please, module authors: if your Makefile.PL is non-trival, include a META.yml file!)
    You do so by using a modern ExtUtils::MakeMaker. Since version 6.11 of July 2003 (and many beta versions before then) a make dist will create a META.yml file and add it to your MANIFEST. See the MakeMaker FAQ.