#!/usr/bin/perl -w

# apt-rdepends
#
# apt-rdepends performs recursive dependency listings similar to apt-cache.
# Copyright (C) 2002-2004  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 $version = "1.2.0";

my $man = 0;
my $help = 0;
my $ver = 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;
# Are we following Build-Depends?
my $builddep = 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,
	    'build-depends|b'  => \$builddep,
	    'dotty|d'          => \$dotty,
	    'follow|f=s'       => \@follow,
	    'show|s=s'         => \@show,
	    'print-state|p'    => \$printstate,
	    'state-follow=s'   => \@statefollow,
	    'state-show=s'     => \@stateshow,
	    'help|h|?'         => \$help,
	    'version'          => \$ver,
	    'man'              => \$man) or pod2usage(verbose => 0);
print_version() if ($ver);
pod2usage(verbose => 2) if ($man);
pod2usage(verbose => 0) if ($help);
pod2usage(verbose => 0) unless (scalar(@ARGV));
if ($reverse && $builddep) {
  print(STDERR "E: Reverse build-dependencies are not supported\n");
  exit 100;
}

# Tokenize the argument lists
@follow = split(/,/,join(',',@follow));
@show = split(/,/,join(',',@show  ));
@statefollow = split(/,/,join(',',@statefollow));
@stateshow = split(/,/,join(',',@stateshow  ));

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

# 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::Source;
use AptPkg::Cache;
use AptPkg::Source;
$_config->init();
$_system = $_config->system();

# Choose whether we're searching Depends or Build-Depends.
my $cache = AptPkg::Cache->new();
my $source;
if ($builddep) {
  $source = AptPkg::Source->new();
}

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

# Set defaults if they weren't defined on the command line.
if ($builddep) {
  @follow = ("Build-Depends", "Build-Depends-Indep") unless (@follow);
  @show = ("Build-Depends", "Build-Depends-Indep") unless (@show);
}
else {
  @follow = (AptPkg::Dep::Depends . "") unless (@follow);
  @show = (AptPkg::Dep::Depends . "") unless (@show);
}
my %deptype_dict;

@statefollow = ("NotInstalled",
		"UnPacked",
		"HalfConfigured",
		"HalfInstalled",
		"ConfigFiles",
		"Installed")
  unless (@statefollow);
@stateshow = ("NotInstalled",
	      "UnPacked",
	      "HalfConfigured",
	      "HalfInstalled",
	      "ConfigFiles",
	      "Installed")
  unless (@stateshow);

# 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;

sub print_depcompareop {
  my $depcompareop = shift(@_);

  if ($depcompareop == AptPkg::Dep::Or) {
    return "|";
  }
  if ($depcompareop == AptPkg::Dep::NoOp) {
    return "";
  }
  if ($depcompareop == AptPkg::Dep::LessEq) {
    return "<=";
  }
  if ($depcompareop == AptPkg::Dep::GreaterEq) {
    return ">=";
  }
  if ($depcompareop == AptPkg::Dep::Less) {
    return "<<";
  }
  if ($depcompareop == AptPkg::Dep::Greater) {
    return ">>";
  }
  if ($depcompareop == AptPkg::Dep::Equals) {
    return "=";
  }
  if ($depcompareop == AptPkg::Dep::NotEquals) {
    return "!=";
  }
}

sub get_depends {
  my $pkg = shift(@_);
  my $reverse = shift(@_);

  # Resolve the package by name.
  my $p;
  if ($builddep) {
    unless ($p = $source->get($pkg)) {
      warn "W: Unable to locate package $pkg\n";
      return;
    }
  }
  else {
    unless ($p = $cache->get($pkg)) {
      warn "W: Unable to locate package $pkg\n";
      return;
    }
  }

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

sub print_package {
  my $pkg = shift(@_);
  my $dotty = shift(@_);
  my $results = shift(@_);

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

  # Display the list of dependencies
  for my $deptype (sort keys %$results) {
    # Should we show this?
    if (grep {$deptype eq $_} @show
	or grep {$deptype_dict{$deptype} eq lc($_)} @show) {
      for my $parent (sort keys %{$$results{$deptype}}) {
	# Capture the state and ensure we only show the things requested.
	my $state = $$results{$deptype}{$parent}[1];
	my $dep = $$results{$deptype}{$parent}[2];
	if (grep {$state eq $_} @stateshow) {
	  unless ($dotty) {
	    print("  $DirText$dep", ": $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 == AptPkg::Dep::Conflicts) {
		print('[color=springgreen]');
		last SPRING;
	      }
	      if ($deptype == AptPkg::Dep::Depends) {
		last SPRING;
	      }
	      if ($deptype == AptPkg::Dep::Suggests) {
		print('[color=yellow]');
		last SPRING;
	      }
	      if ($deptype == AptPkg::Dep::Recommends) {
		print('[color=orange]');
		last SPRING;
	      }
	      if ($deptype == AptPkg::Dep::Replaces) {
		print('[color=red]');
		last SPRING;
	      }
	      if ($deptype == AptPkg::Dep::PreDepends) {
		print('[color=blue]');
		last SPRING;
	      }
	    }
	    }
	    print(";\n");
	  }
	}
      }
    }
  }
}

sub file_depends {
  my $results = shift(@_);
  my $deps = shift(@_);

  for my $dep (@$deps) {
    # Figure out the version.
    my $version = ($reverse
		   ? $dep->{ParentVer}->{VerStr}
		   : $dep->{TargetVer});
    if ($version) {
      $version = ($dep->{CompTypeDeb}
		  ? $dep->{CompTypeDeb} . " "
		  : "") . $version;
    }
    # Figure out the current state.
    my $state = ($reverse
		 ? $dep->{ParentPkg}->{CurrentState}
		 : $dep->{TargetPkg}->{CurrentState});
    # Push the name of this package into the right pigeonhole.
    $$results{0+$dep->{DepType}}{$dep->{$PkgReference}->{Name}} =
      [ $version, $state, "$dep->{DepType}"];
    # Populate the dictionaries of names for this lcoale
    $deptype_dict{0+$dep->{DepType}} = lc("$dep->{DepType}");
  }
}

sub file_builddepends {
  my $results = shift(@_);
  my $deps = shift(@_);

  # Build-Depends are keyed by dependency type
  for my $deptype (keys %$deps) {
    # There is a list of packages within each
    for my $pkgs ($$deps{$deptype}) {
      # Each package may have a version
      for my $pkg (@$pkgs) {
	my $version = $pkg->[2];
	if ($version) {
	  $version = ($pkg->[1]
		      ? print_depcompareop($pkg->[1]) . " "
		      : "") . $version;
	}
	my $p = $cache->get($pkg->[0]);
	my $state = "Unknown";
	$state = $cache->get($pkg->[0])->{CurrentState} if ($p);
	# Push the name of this package into the right pigeonhole.
	$$results{$deptype}{$pkg->[0]} = [ $version, $state, "$deptype" ];
	$deptype_dict{$deptype} = lc($deptype);
      }
    }
  }
}

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

      # Get the dependencies for this $pkg
      my $deps = get_depends($pkg, $reverse) or next;

      # %results stores results for each category of dependency (Conflicts,
      # Depends, Replaces, Suggests)
      my %results;
      if ($builddep) {
	file_builddepends(\%results, $deps);
      }
      else {
	file_depends(\%results, $deps);
      }

      # Display the information we have gathered about this package.
      print_package($pkg, $dotty, \%results);

      # 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.
	}
      }

    }
  }
}

sub print_version {
  print("apt-rdepends $version\n");
  print("Written by Simon Law.\n");
  print("\n");
  print("Copyright (C) 2002-2004  Simon Law\n");
  print("This is free software; see the source for copying conditions.");
  print("  There is NO\nwarranty; not even for MERCHANTABILITY or");
  print(" FITNESS FOR A PARTICULAR PURPOSE.\n\n");
  exit 3;
}

# Main section
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> ...]

=begin text

Options:

=over -1

 -b, --build-depends        show build dependencies
 -d, --dotty                generates a dotty graph
 -p, --print-state          show the state of each dependency
 -r, --reverse              list packages that depend on the specified one
 -f, --follow=DEPENDS       only follow DEPENDS dependencies recursively
 -s, --show=DEPENDS         only show DEPENDS dependencies
     --state-follow=STATES  only follow STATES states recursively
     --state-show=STATES    only show STATES states
     --help                 display this help and exit
     --man                  display the man page and exit
     --version              output version information and exit

=back

=end text

=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<-b>, B<--build-depends>

Show build dependencies instead of normal package dependencies.

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

dotty takes a list of packages on the command line and generates
output suitable for use by springgraph.  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.

The possible values for I<DEPENDS> are: I<Depends>, I<PreDepends>,
I<Suggests>, I<Recommends>, I<Conflicts>, I<Replaces>, and
I<Obsoletes>.

In B<--build-depends> mode, the possible values are: I<Build-Depends>,
I<Build-Depends-Indep>, I<Build-Conflicts>, I<Build-Conflicts-Indep>.

=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<Unknown>, 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> cannot do reverse build-dependencies.  This is really
difficult, since it would have to load the whole cache into memory
before discovering which packages depend on others to build.

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@debian.org>

=cut

# Local Variables:
# perl-continued-statement-offset:2
# perl-indent-level:2
# End:

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