#!/usr/bin/perl -w

# apt-rdepends, version 1.0.0
#
# apt-rdepends performs recursive dependency listings similar to apt-cache.
# Copyright (C) 2002  Simon Law
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

use strict;

use Getopt::Long qw(:config gnu_getopt);
use Pod::Usage;

my $man = 0;
my $help = 0;
## Parse options and print usage if there is a syntax error,
## or if usage was explicitly requested.

# Choose the direction of our recursive dependencies
my $reverse = 0;
# Do we graph a graphviz graph?
my $dotty = 0;
# Which types of dependencies do we follow?
my @follow = ();
# Which types of dependencies do we show?
my @show = ();

# We don't print package states by default.
my $printstate = 0;
# Which states should I follow?
my @statefollow = ();
# Which states should I show?
my @stateshow = ();

GetOptions ('reverse|r'        => \$reverse, 
            'dotty|d'          => \$dotty, 
            'follow|f=s'       => \@follow, 
            'show|s=s'         => \@show,
            'print-state|p'    => \$printstate,
            'state-follow=s'   => \@statefollow,
            'state-show=s'     => \@stateshow,
            'help|?'           => \$help,
            man                => \$man) or pod2usage(2);
pod2usage(1) if $help;
pod2usage(-verbose => 2) if $man;
@follow  = split(/,/,join(',',@follow));
@show    = split(/,/,join(',',@show  ));
@statefollow  = split(/,/,join(',',@statefollow));
@stateshow    = split(/,/,join(',',@stateshow  ));

@follow = ('Depends') unless @follow;
@show = ('Depends') unless @show;
@statefollow = ('NotInstalled', 'UnPacked', 'HalfConfigured', 
                'HalfInstalled', 'ConfigFiles', 'Installed') 
  unless @statefollow;
@stateshow = ('NotInstalled', 'UnPacked', 'HalfConfigured', 
              'HalfInstalled', 'ConfigFiles', 'Installed')
  unless @stateshow;

# Finish choosing the direction of our recursive dependencies
my $DirText;
my $PkgReference;
if ($reverse) {
  $DirText = 'Reverse ';
  $PkgReference = 'ParentPkg';
}
else {
  $DirText = '';
  $PkgReference = 'TargetPkg';
}

# We will track all the packages we have ever seen in this hash.  
# Therefore, we will never duplicate the display of an entry.
my %seen;

# Redirect AptPkg's little ditty so it doesn't go into our files
open(OLDOUT, ">&STDOUT");
open(STDOUT, ">&STDERR");
select(STDERR); $| = 1;

# Initialise AptPkg's interface. 
use AptPkg::Config '$_config';
use AptPkg::System '$_system';
use AptPkg::Cache
$_config->init;
$_system = $_config->system;
my $cache = AptPkg::Cache->new;

# Restore the redirects.
close(STDOUT);
open(STDOUT, ">&OLDOUT");
close(OLDOUT);
select(STDOUT); $| = 0;

sub show_rdepends {
  for my $pkg (@_) {
    # Only recurse if we have never seen this package before
    unless ($seen{$pkg}) {
      $seen{$pkg} = 1;

      my $p;
      unless ($p = $cache->{$pkg}) {
        warn "W: Unable to locate package $pkg\n";
        next;
      }

      # Which way do our dependencies go?  Reverse, or forward.  Notice 
      # how we get the last version for our forward dependencies.
      my $deps;
      if ($reverse) {
        $deps = $p->{RevDependsList};
      }
      else {
        if (my $i = $p->{VersionList}) {
          $deps = (pop @$i)->{DependsList};
        }
      }

      # Display the name of the package.
      if ($dotty) {
        print "\"$pkg\" [shape=box];\n";
      }
      else {
        print "$pkg\n";
      }

      # %results stores results for each category of dependency (Conflicts,
      # Depends, Replaces, Suggests)
      my %results;
      for (@$deps) {
        # Figure out the version.
        my $version =   $reverse
                      ? $_->{ParentVer}->{VerStr}
                      : $_->{TargetVer};
        if ($version) {
          $version = (  $_->{CompTypeDeb} 
                      ? $_->{CompTypeDeb} . " " 
                      : "")
                     . $version;
        }
        # Figure out the current state.
        my $state =   $reverse
                    ? $_->{ParentPkg}->{CurrentState}
                    : $_->{TargetPkg}->{CurrentState};
        # Push the name of this package into the right pigeonhole.
        $results{$_->{DepType}}{$_->{$PkgReference}->{Name}}
          = [ $version, $state ];
      }

      # Display this list.
      for my $deptype (sort keys %results) {
        if (grep {$deptype eq $_} @show) {
          for my $parent (sort keys %{$results{$deptype}}) {
            unless ($dotty) {
              my $state = $results{$deptype}{$parent}[1];
              if (grep {$state eq $_} @stateshow) {
                print "  $DirText$deptype", ": $parent";
                if (my $ver = $results{$deptype}{$parent}[0]) {
                  print " ($ver)";
                }
                if ($printstate) {
                  print " [$state]";
                }
                print "\n";
              }
            }
           else {
              my $target = $parent;
              if (my $ver = $results{$deptype}{$parent}[0]) {
                $target .= " ($ver)";
              }
              if ($printstate) {
                $target .= " [" . $results{$deptype}{$parent}[1] . "]";
              }
              if ($reverse) {
                print "\"$parent\" -> \"$pkg\"";
              }
              else {
                print "\"$pkg\" -> \"$parent\"";
              }
              SPRING: {
                if ($deptype eq 'Conflicts')  { print '[color=springgreen]'; 
                                                last SPRING; }
                if ($deptype eq 'Depends')    { last SPRING; }
                if ($deptype eq 'Suggests')   { print '[color=yellow]'; 
                                                last SPRING; }
                if ($deptype eq 'Recommends') { print '[color=orange]'; 
                                                last SPRING; }
                if ($deptype eq 'Replaces')   { print '[color=red]'; 
                                                last SPRING; }
                if ($deptype eq 'Predepends') { print '[color=blue]'; 
                                                last SPRING; }
              }
              print ";\n";
            }
          }
        }
      }

      # Recurse through the packages mentioned as dependencies.
      for my $deptype (sort keys %results) {
        # Here, we filter the types of dependencies to follow.
        if (grep {$deptype eq $_} @follow) {
          show_rdepends (
            grep {
              my $state = $results{$deptype}{$_}[1];
              grep {
                $state eq $_
              } @statefollow
            } sort keys %{$results{$deptype}}
          );
          # I feel the need to explain this hairy double grep argument.
          # We need to pass an array of package names to show_rdepends,
          # which will recursively follow them for further processing.
          # However, we want to filter this list so that we have the
          # inner join of @statefollow and the state of each package.
          # Hence, the double grep.
        }
      }

    }
  }
}

if ($dotty) { 
  print "digraph packages {\n";
  print "concentrate=true;\n";
  print "size=\"30,40\";\n";
}
show_rdepends @ARGV;
if ($dotty) { print "}\n"; }

__END__

=head1 NAME

apt-rdepends - performs recursive dependency listings similar to apt-cache

=head1 SYNOPSIS

apt-rdepends [options] [I<pkgs> ...]

=head1 DESCRIPTION

B<apt-rdepends> searches through the APT cache to find package
dependencies.  B<apt-rdepends> knows how to emulate the result
of calling B<apt-cache> with both I<depends> and I<dotty>
options.

By default, B<apt-rdepends> shows a listing of each dependency 
a package has.  It will also look at each of these fulfilling packages, 
and recursively lists their dependecies.

=head1 OPTIONS

=over 8

=item B<-d>, B<--dotty>

dotty takes a list of packages on the command line and 
gernerates output suitable for use by dotty from the 
GraphVis <URL:http://www.research.att.com/sw/tools/graphviz/>
package. The result will be a set of nodes and edges representing the 
relationships between the packages. By default the given packages will 
trace out all dependent packages which can produce a very large graph. 

Blue lines are pre-depends, green lines are conflicts, yellow lines 
are suggests, orange lines are recommends, red lines are replaces, 
and black lines are depends.

Caution, dotty cannot graph larger sets of packages.

=item B<-p>, B<--print-state>

Shows the state of each dependency after each package version.
See B<--state-follow> and B<--state-show> for why this is useful.

=item B<-r>, B<--reverse>

Shows the listings of each package that depends on a package.
Furthermore, it will look at these dependent packages, and find their
dependers.

=item B<-f>, B<--follow=>I<DEPENDS>

A comma-separated list of I<DEPENDS> types to follow recursively.  
By default, it only follows the I<Depends> type.

=item B<-s>, B<--show=>I<DEPENDS>

A comma-separated list of I<DEPENDS> types to show, when displaying 
a listing.  By default, it only shows the I<Depends> type.

=item B<--state-follow=>I<STATES>

=item B<--state-show=>I<STATES>

These two options are similar to B<--follow> and B<--show>.  They both
deal with the current state of a package.  By default, the value of
I<STATES> is I<NotInstalled>, I<UnPacked>, I<HalfConfigured>,
I<HalfInstalled>, I<ConfigFiles>, and I<Installed>.

These options are useful, if you only want to only look at the 
dependencies between the I<Installed> packages on your system.  You
can then call:

=over 4

apt-rdepends --state-follow=Installed libfoo

=back

Or if you want to only show the packages installed on your system:

=over 4

apt-rdepends --state-follow=Installed --state-show=Installed libfoo

=back

=item I<pkgs>

The list of packages on which to discover dependencies.

=back

=head1 SEE ALSO

I<apt.conf>(5), I<sources.list>(5), B<apt-cache>(8), I<AptPkg>(3)

=head1 BUGS

B<apt-rdepends> does not emulate B<apt-cache> perfectly.  It does
not display information about virtual packages, nor does it know about
virtual packages when it is in reverse dependency mode.

B<apt-rdepends> also does not know how to stop after a certain depth
has been reached.

B<apt-rdepends> exists.  This functionality should really reside in
B<apt-cache> itself.

=head1 AUTHOR

B<apt-rdepends> was written by Simon Law <sfllaw@engmail.uwaterloo.ca>

=cut

# vim: set ts=2 et sw=2 si:
