Files
pkgsrc-ng/pkgtools/pkgdepgraph/files/pkgdepgraph.pl
2014-11-05 12:41:07 +01:00

738 lines
18 KiB
Perl
Executable File

#!@PERL5@
# Copyright (c) 2002, 2003, 2004 by Andrew Brown <atatat@netbsd.org>
# Absolutely no warranty.
# $NetBSD: pkgdepgraph.pl,v 1.12 2014/03/03 05:06:43 obache Exp $
# pkgdepgraph: @DISTVER@
use strict;
# no strict 'refs';
use Getopt::Long;
Getopt::Long::Configure("bundling");
my(@opts, %opt);
my($iam, $version, $usecolor, $group, $locations, $order, $versions);
my($limit, $delete, $rebuild, $force, @outofdate, @update, $clean);
my($pkg_dbdir, $pkgsrcdir, $packages, $pkgadd, $fetch, $make);
my($all, $target, $exists, $reverse, $simple, @subgraph, @impact, %impactof);
$version = '@DISTVER@';
($iam = $0) =~ s:.*/::;
@opts = ('A', 'a+', 'C', 'c', 'D', 'd=s', 'e', 'F', 'f', 'g', 'i=s',
'K=s', 'L', 'l', 'M=s', 'm=s', 'O=s', 'o', 'P=s', 'R', 'r',
'S=s', 's', 't=s', 'U=s', 'v');
%opt = (
'A' => \$pkgadd,
'a' => \$all,
# 'C' => implies "realclean", handled later
# 'c' => implies "clean", handled later
'D' => \$delete,
'd' => \$pkg_dbdir,
'e' => \$exists,
'F' => \$fetch,
'f' => \$force,
'g' => \$group,
'i' => \@impact,
'K' => \$packages,
'L' => \$limit,
'l' => \$locations,
'M' => \$make,
'm' => \$target,
'O' => \@outofdate,
'o' => \$order,
'P' => \$pkgsrcdir,
'R' => \$rebuild,
'r' => \$reverse,
'S' => \@subgraph,
's' => \$simple,
# 't' => goes to rebuild, handled later
'U' => \@update,
'v' => \$versions,
);
die("usage: $iam [-AaCcDeFfgLloRrsv] [-d pkg_dbdir] [-i impact]\n",
" " x (length($iam) + 8),
"[-K packages] [-M make] [-m target] [-O package]\n",
" " x (length($iam) + 8),
"[-P pkgsrcdir] [-S package] [-t target] [-U package]\n",
" " x (length($iam) + 8),
"[data ...]\n")
if (!GetOptions(\%opt, @opts));
die("$iam: -D, -F, -m, and -R are mutually exclusive -- please pick one\n")
if (($delete != 0) +
($fetch != 0) +
($target ne "") +
($rebuild ne "") > 1);
$pkg_dbdir ||= $ENV{'PKG_DBDIR'} || "@PKG_DBDIR@";
$pkgsrcdir ||= $ENV{'PKGSRCDIR'} || "@PKGSRCDIR@";
$packages = $ENV{'PKG_PATH'} if (!$packages);
$packages = $ENV{'PACKAGES'} . "/All" if (!$packages && $ENV{'PACKAGES'});
$packages = $pkgsrcdir . "/packages/All" if (!$packages);
$rebuild &&= $opt{t} || "install";
$clean = "clean" if ($opt{c});
$clean = "CLEANDEPENDS=YES clean" if ($opt{C});
$make ||= $ENV{'MAKE'} || "make";
my(@pkgs, $pkg, $req, %req, %dep, @reqs, @rreqs);
my(%clusters, $cluster);
my(%where, $pkgcnt, $num, %num, @num, %ord, $suffix);
my(%color, $color, %vuln);
my(%need, %forced, $label);
my($recolor, @graph);
my(%vpkgs);
# @pkgs - list of all installed pkgs
# %req - pkg to ref to hash of pkgs that it requires
# %dep - pkg to ref to hash of pkgs that depend on it
# %clusters - pkg prefix to number of pkgs that share the prefix
# %where - pkg to location in source tree
# %num/@num - pkg to group number/group number array ref
# %ord - pkg to its height in the tree
# %color - pkg to pkg color (green, yellow, red, etc)
# %vuln - pkg to vulnerabilities recorded against it
# %need - pkg to version required (pkg is out of date)
# %forced - pkg marked as "forced" to be out of date
# %vpkgs - pkg is viewable (part of selected subgraph)
##
## load out-of-date or security problem list (if given), or a graph to
## recolor
##
$recolor = 0;
if (@ARGV || ! -t) {
$usecolor = 1;
while (<>) {
if (/^digraph/) {
$recolor = 1;
@graph = ($_);
}
elsif ($recolor > 0) {
push(@graph, $_);
$recolor++ if (/^subgraph/);
$recolor-- if (/^\}/);
$recolor -= ($recolor == 0);
}
elsif (m:^([^/\s]+)\t([^/\s]+/[^/\s]+)\t(\d+[^/\s]*)$:) {
$where{"$1-$3"} = $2;
}
elsif (/^Version mismatch: '(\S+)' (\S+) vs (\S+)/) {
$color{"$1-$2"} = "red";
$need{"$1-$2"} = "$1-$3";
}
elsif (/^Unknown package: '(\S+)' version (\S+)/) {
$color{"$1-$2"} = "purple";
}
elsif (/Package (\S+) has a (\S+) vulnerability/) {
$vuln{$1} = $2;
$color{$1} = "red";
}
}
}
##
## load pkg list
##
opendir(P, $pkg_dbdir) || die("opendir");
@pkgs = grep(/-/ && -d "$pkg_dbdir/$_" && -f "$pkg_dbdir/$_/+BUILD_INFO",
readdir(P));
closedir(P);
$pkgcnt = @pkgs;
##
## where are they needed
##
foreach $pkg (@pkgs) {
$where{$pkg} ||= $pkg;
open(R, "<$pkg_dbdir/$pkg/+BUILD_INFO") ||
die("$pkg: +BUILD_INFO: $!\n");
while (<R>) {
if (/^PKGPATH\s*=\s*(\S+)/) {
$where{$pkg} = $1 if ($where{$pkg} eq $pkg);
last;
}
}
close(R);
next if (!open(R, "<$pkg_dbdir/$pkg/+REQUIRED_BY"));
while ($req = <R>) {
chomp($req);
$req{$req}->{$pkg} = 1;
$dep{$pkg}->{$req} = 1;
}
close(R);
}
##
## reset %where based on "better" information, if we have it
##
foreach $pkg (@pkgs) {
if ($need{$pkg} && $where{$need{$pkg}}) {
$where{$pkg} = $where{$need{$pkg}};
}
}
##
## if we're recoloring an existing graph, recolor it now and finish
##
if ($recolor) {
my(%over, %nver, @label, $ocolor);
map({ /(.*)-(.*)/ && ($nver{$1} = $2) } @pkgs);
foreach (@graph) {
# we don't recolor edges
($pkg) = (/\"([^\"]+)\"/);
$pkg =~ s/(.*)-(.*)/$1/;
$over{$pkg} = $2;
if (/, EDGE$/) {
if (defined($nver{$pkg})) {
s/color=\"[^\"]+\"/color=\"green\"/;
}
else {
s/color=\"[^\"]+\"/color=\"black\"/;
}
}
elsif (/label=/) {
s/color=\"([^\"]+)\"/color="NEWCOLOR"/;
$ocolor = $1;
s/label=\"([^\"]+)\"/label="NEWLABEL"/;
$label = $1;
if ($nver{$pkg}) {
if ($nver{$pkg} ne $over{$pkg} || $ocolor ne "red") {
s/NEWCOLOR/green/;
}
else {
s/NEWCOLOR/$ocolor/;
}
@label = split(/\\n/, $label);
$label = "";
# "where" tag
if ($label[0] =~ m:/:) {
$label .= "\\n" . shift(@label);
}
# installed pkg
$label[0] =~ s/(.*$pkg)-\S*$/$1-$nver{$pkg}/ if ($nver{$pkg});
$label .= "\\n" . shift(@label);
# "needed" pkg
if ($label[0] =~ /^$pkg-(.*)/) {
$label .= "\\n$label[0]" if ($1 ne $nver{$pkg});
shift(@label);
}
# there shouldn't be anything left, but...
$label .= "\\n" . join("\\n", @label);
$label =~ s/\\n//;
}
else {
s/NEWCOLOR/black/;
}
s/NEWLABEL/$label/;
}
print;
}
exit(0);
}
##
## eliminate redundancies by deleting edges that are redundant
##
foreach $pkg (@pkgs) {
@reqs = sort(keys %{$req{$pkg}});
@rreqs = recurse(\%req, @reqs);
map(delete($req{$pkg}->{$_}), @rreqs);
@reqs = sort(keys %{$dep{$pkg}});
@rreqs = recurse(\%dep, @reqs);
map(delete($dep{$pkg}->{$_}), @rreqs);
}
##
## create a hash of clusters of package prefixes, with counts. later,
## clusters that have more than one member can be marked as subgraphs.
##
## the outer map() iterates over each pkg name after all instances of
## _ in the pkg name have been changed to - (for the purposes of
## accurate clustering). the inner map() breaks each pkg name up into
## tokens that end in - and loops over the resulting list, appending
## each one to $a. for example:
##
## pkg: one_two-three-4.56
## tokens: one- two- three-
## $a: one- one-two- one-two-three-
##
map({ $a = "";
($b = $_) =~ s/_/-/g;
map({ $a .= $_; $clusters{$a}++; }
$b =~ /([^-]*-)/g); }
@pkgs);
##
## impose some sort of order on the pkgs by assigning them numbers
## that indicate their height in the graph. leaf pkgs will always
## have an order of 1, and each pkg above will be numbered at least 2
## (possibly higher, if there exists another longer path to another
## leaf).
##
map(order(1, $_), @pkgs);
##
## assign each pkg a group number, and count the number of pkgs in
## that group. the group numbers are arbitrary, and serve only to
## identify pkgs that belong to the same group.
##
$num = 1;
foreach $pkg (@pkgs) {
my($pkgnum);
# my direct requirements
@reqs = sort(keys %{$req{$pkg}});
# all the requirements of my requirements
@rreqs = recurse(\%req, @reqs);
# the lowest group number from all of those
$pkgnum = number($pkg, @reqs, @rreqs) || $num;
# stuff all those into the list for that group
push(@{$num[$pkgnum]}, $pkg, @reqs, @rreqs);
# now check for packages coming from other groups
foreach $req ($pkg, @reqs, @rreqs) {
# no group yet, skip on
next if (!$num{$req});
# was $req in a different group
if ($num{$req} != $pkgnum) {
# yes, pull that group into the current group
push(@{$num[$pkgnum]}, @{$num[$num{$req}]});
# empty out the old group
@{$num[$num{$req}]} = ();
}
}
# reduce the group list
@{$num[$pkgnum]} = uniq(sort(@{$num[$pkgnum]}));
# make sure all packages in this group know
map($num{$_} = $pkgnum, @{$num[$pkgnum]});
# skip to next available group number
$num += ($num == $pkgnum);
}
##
## if we want to check a specific pkg for rebuild impact, mark it as
## "forced" to be out of date, unless it already *is* out of date.
##
if (@outofdate) {
$usecolor = 1;
canonicalize(@outofdate);
foreach (@outofdate) {
if ($color{$_} ne "red") {
$color{$_} = "red";
$need{$_} = $_;
$forced{$_} = " (forced)";
}
}
}
##
## if we want to update a specific package, mark all non-related
## packages as "green". this avoids rebuilding unnecessary pkgs that
## don't depend on any of the same dependencies as the given pkg. if
## $force is set, mark *all* dependencies of the given pkg as out of
## date.
##
if (@update) {
my(@leftover);
canonicalize(@update);
@update = uniq(sort(@update, recurse(\%req, @update)));
if ($force) {
foreach (@update) {
if ($color{$_} ne "red") {
$color{$_} = "red";
$need{$_} = $_;
$forced{$_} = " (forced)";
}
}
}
foreach (sort(@pkgs)) {
if ($_ eq $update[0]) {
shift(@update);
}
else {
push(@leftover, $_);
}
}
delete(@color{@leftover});
delete(@need{@leftover});
}
##
## pick packages for a subgraph
##
## + means up from given package, - means down, ++ means all the way
## up, -- means all the way down, = means all "connected" packages, etc.
##
if (@subgraph) {
my ($sub, $up, $down, $eq);
foreach (@subgraph) {
($sub) = (/^([-+=]+)/);
s/^[-+=]+//;
$sub = "+-" if ($sub eq "");
canonicalize($_);
$up = join("", ($sub =~ /(\+)/g));
$down = join("", ($sub =~ /(-)/g));
$eq = join("", ($sub =~ /(=)/g));
if ($eq) {
map($vpkgs{$_} = 1, @{$num[$num{$_}]});
}
else {
if ($up) {
@reqs = sort(keys %{$req{$_}});
@rreqs = (length($up) > 1) ? recurse(\%req, @reqs) : ();
map($vpkgs{$_} = 1, ($_, @reqs, @rreqs));
}
if ($down) {
@reqs = sort(keys %{$dep{$_}});
@rreqs = (length($down) > 1) ? recurse(\%dep, @reqs) : ();
map($vpkgs{$_} = 1, ($_, @reqs, @rreqs));
}
}
}
}
else {
@vpkgs{@pkgs} = (1) x @pkgs;
}
##
## if checking for rebuild impact, also mark packages that are too
## deeply involved as "green" so that they're not candidates for
## destruction
##
if (@impact) {
my ($impact);
# step 1: canonicalize anything that's not a number (ie, is the
# name of a pkg) and eliminate duplicates (we just don't need 'em)
foreach (@impact) {
next if (/^\d+$/);
canonicalize($_);
}
@impact = uniq(sort(@impact));
# step 2: the "default" impact allows for anything to be rebuilt,
# but numeric values in @impact are also allowed, so pick the
# lowest one (specifying both 1 and 2 really means just 1)
$impact = $ord{(sort(byord @pkgs))[0]};
while ($impact[0] =~ /^\d+$/) {
$_ = shift(@impact);
$impact = $_ if ($_ < $impact);
}
# step 3: anything that would have too great an impact on the tree
# gets marked (the impactof() function will check the @impact
# array to avoid specific pkgs being rebuild)
foreach $pkg (keys %vpkgs) {
next if (impactof($impact, $pkg) <= $impact);
$vpkgs{$pkg} = 2;
}
# step 4: anything so marked gets tagged as green. this tagging
# is a separate step so that we can properly judge impact over the
# entire tree (marking too early could prematurely split chunks
# that need to be rebuilt)
foreach $pkg (keys %vpkgs) {
$color{$pkg} = "green" if ($vpkgs{$pkg} == 2);
}
}
##
## translate "older" alternate output modes to the new generic version
##
if ($fetch) {
$target = "fetch";
}
elsif ($rebuild) {
$exists = 1;
$limit = 1;
$target = $rebuild;
}
elsif ($delete) {
$all++;
$simple = 1;
}
##
## "target" output mode, ordered by ascendency
##
if ($target || $simple) {
my(@targets);
printf("PKG_PATH=\"$packages\"\nexport PKG_PATH\n")
if ($pkgadd && $rebuild);
@targets = grep((color($_) eq "red" && !$limit) ||
(color($_) ne "green" &&
($all || ($ord{$_} == 1 && $limit))) ||
($all > 1), keys %vpkgs);
@targets = sort(byord @targets);
@targets = reverse(@targets) if (!$reverse);
print_package(@targets);
print("true\n") if (!$simple);
exit(0);
}
##
## show left overs as a graph
##
printf("digraph \"%s packages\" {\n",
$limit ? "out of date" : "installed");
printf("label = \"%s packages %s, generated by %s v%s, on %s\";\n",
$limit ? "out of date" : "installed",
@subgraph ? "subgraph (@subgraph)" : "graph",
$iam, $version, scalar(localtime));
foreach $pkg (sort(bynum keys %vpkgs)) {
$color = color($pkg);
next if ($limit && $color eq "green");
$label = $pkg;
$label =~ s/(.*)-.*/$1/ if (!$versions);
$label = "($ord{$pkg}) $label" if ($order);
$label = "$where{$pkg}\\n$label" if ($locations);
$label .= "\\n$need{$pkg}$forced{$pkg}" if ($need{$pkg});
if ($vuln{$pkg}) {
$label .= "\\n(no update available)" if (!$need{$pkg});
$label .= "\\n[$vuln{$pkg}]";
}
$suffix = "\t// \#$ord{$pkg}, group $num{$pkg}, " .
(exists($impactof{$pkg}) ? "impact $impactof{$pkg}, " : "") .
scalar(@{$num[$num{$pkg}]}) . " members, $pkgcnt pkgs";
$suffix .= ", LEAF" if ($ord{$pkg} == 1);
##
## scan the cluster list, but in the opposite order so in the case
## of pkgs with a common "multi-token" prefix, we only emit the
## one with the longest name. we have to prepend the names to a
## buffer so that they end up being printed in the reverse of
## discovery order, so that we end up with the "least-specific"
## subgroup announced first.
##
($a = $pkg) =~ s/_/-/g;
$b = 1;
$cluster = "";
while ($group && $a =~ s/-[^-]+-?$/-/) {
next if ($clusters{$a} == $b);
$b = $clusters{$a};
$cluster = sprintf("subgraph \"cluster_%s\" {\n", substr($a, 0, -1)) .
sprintf("label = \"%s (%d)\";\n", substr($a, 0, -1), $b) .
$cluster;
}
print($cluster);
printf("\"%s\" [color=\"%s\",label=\"%s\"];$suffix\n", $pkg,
$usecolor ? $color : "black", $label);
$cluster =~ s/label = .*\n//g;
$cluster =~ s/.+\{/\}/g;
print($cluster);
@reqs = sort(keys %{$req{$pkg}});
$suffix =~ s/, LEAF$//;
$suffix .= ", EDGE";
foreach $req (@reqs) {
$color = color($req);
next if ($limit && $color eq "green");
printf("\"%s\" -> \"%s\" [color=\"%s\"];$suffix\n", $req, $pkg,
$usecolor ? $color : "black");
}
}
print("}\n");
##
## print sh(1) style commands to handle work on a given package, or
## just the package name if $simple is set
##
sub print_package {
foreach (@_) {
printf("( pkg_info -qe %s || ", /(.*)-.*/) if ($exists && !$simple);
if ($simple) {
print($_);
}
elsif ($pkgadd) {
printf("( pkg_add %s.tgz", ($need{$_} || $_));
}
else {
print("( cd $pkgsrcdir/$where{$_} && $make $target");
print(" && $make $clean") if ($clean);
}
if (!$simple) {
print(" )") if ($exists);
print(" ) &&");
}
print("\n");
}
}
##
## find all dependencies above or below a given node
##
sub recurse {
my(@list, @new, $map);
@list = ();
$map = shift;
foreach (@_) {
@new = keys %{$map->{$_}};
push(@list, @new, recurse($map, @new));
}
uniq(sort(@list));
}
##
## canonicalize a pkg name based on what we have installed
##
sub canonicalize {
my($canon, $pkg);
foreach $pkg (@_) {
# attempt to find actual pkg, first by argument given...
($canon) = grep($pkg eq $_, @pkgs);
# ...then by comparing against the internal list sans version numbers
($canon) = grep(($a = $_) =~ s/(.*)-.*/$1/ && $pkg eq $a, @pkgs)
if (!defined($canon));
die("package '$pkg' not found\n")
if (!defined($canon));
$pkg = $canon;
}
@_;
}
##
## lowest group number of a set of packages
##
sub number {
my($n, $pkg);
$n = 0;
foreach $pkg (@_) {
$n = $num{$pkg} if ($n == 0 || $num{$pkg} < $n);
}
$n + 0;
}
##
## pick a color based on the color of the dependencies
##
sub color {
my($pkg) = @_;
if ($color{$pkg}) {
$color{$pkg};
}
else {
my($req, @reqs, $color);
@reqs = sort(keys %{$req{$pkg}});
@reqs = (@reqs, recurse(\%req, @reqs));
$color = "green";
foreach $req (@reqs) {
if ($color{$req} eq "red") {
return "orange";
}
elsif ($color{$req} eq "purple") {
$color = "blue";
}
}
$color;
}
}
##
## bynum - higher numbers come last
##
sub bynum {
return $num{$a} <=> $num{$b} ||
$a cmp $b;
}
##
## byord - higher orders come first
##
sub byord {
return $ord{$b} <=> $ord{$a} ||
$b cmp $a;
}
##
## order - the order of a pkg is one higher than anything below it
##
sub order {
my($n, @pkgs) = @_;
my($pkg);
foreach $pkg (@pkgs) {
$ord{$pkg} = $n if ($ord{$pkg} <= $n);
order($n + 1, sort(keys %{$req{$pkg}}));
}
}
##
## uniq - eliminate adjacent duplicate entries in an array
##
sub uniq {
my($i);
for ($i = 0; $i < $#_; ) {
if ($_[$i] eq $_[$i + 1]) {
splice(@_, $i, 1);
}
else {
$i++;
}
}
@_;
}
##
## impactof - impact of pkg delete/rebuild is the longest path (either
## up or down the tree) that encompasses all things that need
## rebuilding
##
sub impactof {
my ($impact, $pkg) = @_;
my (@in, @out);
# if we already know or it's dead-simple, get out early
return $impactof{$pkg} if (exists($impactof{$pkg}));
return $impactof{$pkg} = 0 if (color($pkg) eq "green");
# starting with the given pkg, repeatedly look up and down the
# tree for connected pkgs that also require a rebuild
@out = ($pkg);
do {
@in = @out;
@out = ($pkg);
push(@out, grep(color($_) ne "green", recurse(\%dep, @in)));
push(@out, grep(color($_) ne "green", recurse(\%req, @out)));
@out = uniq(sort(byord @out));
} while (@in != @out);
# check to see if the set of related pkgs intersects with the set
# we want to avoid and if so, mark this set as "too expensive"
$a = "";
if (@impact) {
foreach $b (@impact) {
if (grep($_ eq $b, @out)) {
$a = $b;
$impact++;
last;
}
}
}
# if we didn't hit anything, the impact is the one less than the
# highest ordered remotely connected pkg we found (the longest
# path from the top to the bottom of the set to be rebuilt)
$impact = $ord{$out[0]} - 1 if ($a eq "");
@impactof{@in} = ($impact) x @in;
$impactof{$pkg};
}