Current File : //bin/dpkg-depcheck |
#!/usr/bin/perl
# Copyright Bill Allombert <ballombe@debian.org> 2001.
# Modifications copyright 2002-2005 Julian Gilbey <jdg@debian.org>
# 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, see <https://www.gnu.org/licenses/>.
use strict;
use warnings;
use 5.006_000; # our() commands
use Cwd;
use File::Basename;
use Getopt::Long;
use Devscripts::Set;
use Devscripts::Packages;
use Devscripts::PackageDeps;
# Function prototypes
sub process_features ($$);
sub getusedfiles (@);
sub filterfiles (@);
# Global options
our %opts;
# A list of files that do not belong to a Debian package but are known
# to never create a dependency
our @known_files = (
"/etc/ld.so.cache", "/etc/dpkg/shlibs.default",
"/etc/dpkg/dpkg.cfg", "/etc/devscripts.conf"
);
# This will be given information about features later on
our (%feature, %default_feature);
my $progname = basename($0);
my $modified_conf_msg;
sub usage () {
my @ed = ("disabled", "enabled");
print <<"EOF";
Usage:
$progname [options] <command>
Run <command> and then output packages used to do this.
Options:
Which packages to report:
-a, --all Report all packages used to run <command>
-b, --build-depends Do not report build-essential or essential packages
used or any of their (direct or indirect)
dependencies
-d, --ignore-dev-deps Do not show packages used which are direct
dependencies of -dev packages used
-m, --min-deps Output a minimal set of packages needed, taking
into account direct dependencies
-m implies -d and both imply -b; -a gives additional dependency information
if used in conjunction with -b, -d or -m
-C, --C-locale Run command with C locale
--no-C-locale Don\'t change locale
-l, --list-files Report list of files used in each package
--no-list-files Do not report list of files used in each package
-o, --output=FILE Output diagnostic to FILE instead of stdout
-O, --strace-output=FILE Write strace output to FILE when tracing <command>
instead of a temporary file
-I, --strace-input=FILE Get strace output from FILE instead of tracing
<command>; strace must be run with -f -q for this
to work
-f, --features=LIST Enable or disabled features given in
comma-separated LIST as follows:
+feature or feature enable feature
-feature disable feature
Known features and default setting:
warn-local ($ed[$default_feature{'warn-local'}]) warn if files in /usr/local are used
discard-check-version ($ed[$default_feature{'discard-check-version'}]) discard execve with only
--version argument; this works around some
configure scripts that check for binaries they
don\'t use
trace-local ($ed[$default_feature{'trace-local'}]) also try to identify file
accesses in /usr/local
catch-alternatives ($ed[$default_feature{'catch-alternatives'}]) catch access to alternatives
discard-sgml-catalogs ($ed[$default_feature{'discard-sgml-catalogs'}]) discard access to SGML
catalogs; some SGML tools read all the
registered catalogs at startup.
--no-conf, --noconf Don\'t read devscripts config files;
must be the first option given
-h, --help Display this help and exit
-v, --version Output version information and exit
Default settings modified by devscripts configuration files:
$modified_conf_msg
EOF
}
sub version () {
print <<'EOF';
This is $progname, from the Debian devscripts package, version 2.22.1ubuntu1
Copyright Bill Allombert <ballombe@debian.org> 2001.
Modifications copyright 2002, 2003 Julian Gilbey <jdg@debian.org>
This program comes with ABSOLUTELY NO WARRANTY.
You are free to redistribute this code under the terms of the
GNU General Public License, version 2 or later.
EOF
}
# Main program
# Features:
# This are heuristics used to speed up the process.
# Since they may be considered as "kludges" or worse "bugs"
# by some, they can be deactivated
# 0 disabled by default, 1 enabled by default.
%feature = (
"warn-local" => 1,
"discard-check-version" => 1,
"trace-local" => 0,
"catch-alternatives" => 1,
"discard-sgml-catalogs" => 1,
);
%default_feature = %feature;
# First process configuration file options, then check for command-line
# options. This is pretty much boilerplate.
if (@ARGV and $ARGV[0] =~ /^--no-?conf$/) {
$modified_conf_msg = " (no configuration files read)";
shift;
} else {
my @config_files = ('/etc/devscripts.conf', '~/.devscripts');
my %config_vars = ('DPKG_DEPCHECK_OPTIONS' => '',);
my %config_default = %config_vars;
my $shell_cmd;
# Set defaults
foreach my $var (keys %config_vars) {
$shell_cmd .= qq[$var="$config_vars{$var}";\n];
}
$shell_cmd .= 'for file in ' . join(" ", @config_files) . "; do\n";
$shell_cmd .= '[ -f $file ] && . $file; done;' . "\n";
# Read back values
foreach my $var (keys %config_vars) { $shell_cmd .= "echo \$$var;\n" }
my $shell_out = `/bin/bash -c '$shell_cmd'`;
@config_vars{ keys %config_vars } = split /\n/, $shell_out, -1;
foreach my $var (sort keys %config_vars) {
if ($config_vars{$var} ne $config_default{$var}) {
$modified_conf_msg .= " $var=$config_vars{$var}\n";
}
}
$modified_conf_msg ||= " (none)\n";
chomp $modified_conf_msg;
if ($config_vars{'DPKG_DEPCHECK_OPTIONS'} ne '') {
unshift @ARGV, split(' ', $config_vars{'DPKG_DEPCHECK_OPTIONS'});
}
}
# Default option:
$opts{"pkgs"} = 'all';
$opts{"allpkgs"} = 0;
Getopt::Long::Configure('bundling', 'require_order');
GetOptions(
"h|help" => sub { usage(); exit; },
"v|version" => sub { version(); exit; },
"a|all" => sub { $opts{"allpkgs"} = 1; },
"b|build-depends" => sub { $opts{"pkgs"} = 'build'; },
"d|ignore-dev-deps" => sub { $opts{"pkgs"} = 'dev'; },
"m|min-deps" => sub { $opts{"pkgs"} = 'min'; },
"C|C-locale" => \$opts{"C"},
"no-C-locale|noC-locale" => sub { $opts{"C"} = 0; },
"l|list-files" => \$opts{"l"},
"no-list-files|nolist-files" => sub { $opts{"l"} = 0; },
"o|output=s" => \$opts{"o"},
"O|strace-output=s" => \$opts{"strace-output"},
"I|strace-input=s" => \$opts{"strace-input"},
"f|features=s" => \&process_features,
"no-conf" => \$opts{"noconf"},
"noconf" => \$opts{"noconf"},
) or do { usage; exit 1; };
if ($opts{"noconf"}) {
die
"$progname: --no-conf is only acceptable as the first command-line option!\n";
}
if ($opts{"pkgs"} eq 'all') {
$opts{"allpkgs"} = 0;
} else {
# We don't initialise the packages database before doing this check,
# as that takes quite some time
unless (system('dpkg -L build-essential >/dev/null 2>&1') >> 8 == 0) {
die
"You must have the build-essential package installed or use the --all option\n";
}
}
@ARGV > 0
or $opts{"strace-input"}
or die
"You need to specify a command! Run $progname --help for more info\n";
# Run the command and trace it to see what's going on
my @usedfiles = getusedfiles(@ARGV);
if ($opts{"o"}) {
$opts{"o"} =~ s%^(\s)%./$1%;
open STDOUT, "> $opts{'o'}"
or warn
"Cannot open $opts{'o'} for writing: $!\nTrying to use stdout instead\n";
} else {
# Visual space
print "\n\n";
print '-' x 70, "\n";
}
# Get each file once only, and drop any we are not interested in.
# Also, expand all symlinks so we get full pathnames of the real file accessed.
@usedfiles = filterfiles(@usedfiles);
# Forget about the few files we are expecting to see but can ignore
@usedfiles = SetMinus(\@usedfiles, \@known_files);
# For a message at the end
my $number_files_used = scalar @usedfiles;
# Initialise the packages database unless --all is given
my $packagedeps;
# @used_ess_files will contain those files used which are in essential packages
my @used_ess_files;
# Exclude essential and build-essential packages?
if ($opts{"pkgs"} ne 'all') {
$packagedeps = Devscripts::PackageDeps->fromStatus();
my @essential = PackagesMatch('^Essential: yes$');
my @essential_packages
= $packagedeps->full_dependencies('build-essential', @essential);
my @essential_files = PackagesToFiles(@essential_packages);
@used_ess_files = SetInter(\@usedfiles, \@essential_files);
@usedfiles = SetMinus(\@usedfiles, \@used_ess_files);
}
# Now let's find out which packages are used...
my @ess_packages = FilesToPackages(@used_ess_files);
my @packages = FilesToPackages(@usedfiles);
my %dep_packages = (); # packages which are depended upon by others
# ... and remove their files from the filelist
if ($opts{"l"}) {
# Have to do it slowly :-(
if ($opts{"allpkgs"}) {
print
"Files used in each of the needed build-essential or essential packages:\n";
foreach my $pkg (sort @ess_packages) {
my @pkgfiles = PackagesToFiles($pkg);
print "Files used in (build-)essential package $pkg:\n ",
join("\n ", SetInter(\@used_ess_files, \@pkgfiles)), "\n";
}
print "\n";
}
print "Files used in each of the needed packages:\n";
foreach my $pkg (sort @packages) {
my @pkgfiles = PackagesToFiles($pkg);
print "Files used in package $pkg:\n ",
join("\n ", SetInter(\@usedfiles, \@pkgfiles)), "\n";
# We take care to note any files used which
# do not appear in any package
@usedfiles = SetMinus(\@usedfiles, \@pkgfiles);
}
print "\n";
} else {
# We take care to note any files used which
# do not appear in any package
my @pkgfiles = PackagesToFiles(@packages);
@usedfiles = SetMinus(\@usedfiles, \@pkgfiles);
}
if ($opts{"pkgs"} eq 'dev') {
# We also remove any direct dependencies of '-dev' packages
my %pkgs;
@pkgs{@packages} = (1) x @packages;
foreach my $pkg (@packages) {
next unless $pkg =~ /-dev$/;
my @deps = $packagedeps->dependencies($pkg);
foreach my $dep (@deps) {
$dep = $$dep[0] if ref $dep;
if (exists $pkgs{$dep}) {
$dep_packages{$dep} = $pkg;
delete $pkgs{$dep};
}
}
}
@packages = keys %pkgs;
} elsif ($opts{"pkgs"} eq 'min') {
# Do a mindep job on the package list
my ($packages_ref, $dep_packages_ref)
= $packagedeps->min_dependencies(@packages);
@packages = @$packages_ref;
%dep_packages = %$dep_packages_ref;
}
print "Summary: $number_files_used files considered.\n" if $opts{"l"};
# Ignore unrecognised /var/... files
@usedfiles = grep !/^\/var\//, @usedfiles;
if (@usedfiles) {
warn "The following files did not appear to belong to any package:\n";
warn join("\n", @usedfiles) . "\n";
}
print "Packages ", ($opts{"pkgs"} eq 'all') ? "used" : "needed", ":\n ";
print join("\n ", @packages), "\n";
if ($opts{"allpkgs"}) {
if (@ess_packages) {
print "\n(Build-)Essential packages used:\n ";
print join("\n ", @ess_packages), "\n";
} else {
print "\nNo (Build-)Essential packages used\n";
}
if (scalar keys %dep_packages) {
print "\nOther packages used with depending packages listed:\n";
foreach my $pkg (sort keys %dep_packages) {
print " $pkg <= $dep_packages{$pkg}\n";
}
}
}
exit 0;
### Subroutines
# This sub is handed two arguments: f or feature, and the setting
sub process_features ($$) {
foreach (split(',', $_[1])) {
my $state = 1;
m/^-/ and $state = 0;
s/^[-+]//;
if (exists $feature{$_}) {
$feature{$_} = $state;
} else {
die("Unknown feature $_\n");
}
}
}
# Get used files. This runs the requested command (given as parameters
# to this sub) under strace and then parses the output, returning a list
# of all absolute filenames successfully opened or execve'd.
sub getusedfiles (@) {
my $file;
if ($opts{"strace-input"}) {
$file = $opts{"strace-input"};
} else {
my $old_locale = $ENV{'LC_ALL'} || undef;
$file = $opts{"strace-output"}
|| `mktemp --tmpdir dpkg-depcheck.XXXXXXXXXX`;
chomp $file;
$file =~ s%^(\s)%./$1%;
my @strace_cmd = (
'strace', '-e', 'trace=open,openat,execve', '-f',
'-q', '-o', $file, @_
);
$ENV{'LC_ALL'} = "C" if $opts{"C"};
system(@strace_cmd);
$? >> 8 == 0
or die "Running strace failed (command line:\n@strace_cmd\n";
if (defined $old_locale) { $ENV{'LC_ALL'} = $old_locale; }
else { delete $ENV{'LC_ALL'}; }
}
my %openfiles = ();
open FILE, $file or die "Cannot open $file for reading: $!\n";
while (<FILE>) {
# We only consider absolute filenames
m/^\d+\s+(\w+)\((?:[\w\d_]*, )?\"(\/.*?)\",.*\) = (-?\d+)/ or next;
my ($syscall, $filename, $status) = ($1, $2, $3);
if ($syscall eq 'open' || $syscall eq 'openat') {
next unless $status >= 0;
} elsif ($syscall eq 'execve') {
next unless $status == 0;
} else {
next;
} # unrecognised syscall
next
if $feature{"discard-check-version"}
and m/execve\(\"\Q$filename\E\", \[\"[^\"]+\", "--version"\], /;
# So it's a real file
$openfiles{$filename} = 1;
}
unlink $file unless $opts{"strace-input"} or $opts{"strace-output"};
return keys %openfiles;
}
# Select those files which we are interested in, as determined by the
# user-specified options
sub filterfiles (@) {
my %files = ();
my %local_files = ();
my %alternatives = ();
my $pwd = cwd();
foreach my $file (@_) {
next unless -f $file;
$file = Cwd::abs_path($file);
my @links = ();
my $prevlink = '';
foreach (ListSymlinks($file, $pwd)) {
if (m%^/(usr|var)/local(/|\z)%) {
$feature{"warn-local"} and $local_files{$_} = 1;
unless ($feature{"trace-local"}) {
$prevlink = $_;
next;
}
} elsif ($feature{"discard-sgml-catalogs"}
and m%^/usr/share/(sgml/.*\.cat|.*/catalog)%) {
next;
} elsif ($feature{"catch-alternatives"} and m%^/etc/alternatives/%)
{
$alternatives{ "$prevlink --> " . readlink($_) } = 1
if $prevlink;
}
$prevlink = $_;
# If it's not in one of these dirs, we skip it
next unless m%^/(bin|etc|lib|sbin|usr|var)%;
push @links, $_;
}
@files{@links} = (1) x @links;
}
if (keys %local_files) {
print "warning: files in /usr/local or /var/local used:\n",
join("\n", sort keys %local_files), "\n";
}
if (keys %alternatives) {
print "warning: alternatives used:\n",
join("\n", sort keys %alternatives), "\n";
}
return keys %files;
}
# The purpose here is to find out all the symlinks crossed by a file access.
# We work from the end of the filename (basename) back towards the root of
# the filename (solving bug#246006 where /usr is a symlink to another
# filesystem), repeating this process until we end up with an absolute
# filename with no symlinks in it. We return a list of all of the
# full filenames encountered.
# For example, if /usr -> /moved/usr, then
# /usr/bin/X11/xapp would yield:
# /usr/bin/X11/xapp, /usr/X11R6/bin/xapp, /moved/usr/X11R6/bin/xapp
# input: file, pwd
# output: if symlink found: (readlink-replaced file, prefix)
# if not: (file, '')
sub NextSymlink ($) {
my $file = shift;
my $filestart = $file;
my $fileend = '';
while ($filestart ne '/') {
if (-l $filestart) {
my $link = readlink($filestart);
my $parent = dirname $filestart;
if ($link =~ m%^/%) { # absolute symlink
return $link . $fileend;
}
while ($link =~ s%^\./%%) { }
# The following is not actually correct: if we have
# /usr -> /moved/usr and /usr/mylib -> ../mylibdir, then
# /usr/mylib should resolve to /moved/mylibdir, not /mylibdir
# But if we try to take this into account, we would need to
# use something like Cwd(), which would immediately resolve
# /usr -> /moved/usr, losing us the opportunity of recognising
# the filename we want. This is a bug we'll probably have to
# cope with.
# One way of doing this correctly would be to have a function
# resolvelink which would recursively resolve any initial ../ in
# symlinks, but no more than that. But I don't really want to
# implement this unless it really proves to be necessary:
# people shouldn't be having evil symlinks like that on their
# system!!
while ($link =~ s%^\.\./%%) { $parent = dirname $parent; }
return $parent . '/' . $link . $fileend;
} else {
$fileend = '/' . basename($filestart) . $fileend;
$filestart = dirname($filestart);
}
}
return undef;
}
# input: file, pwd
# output: list of full filenames encountered en route
sub ListSymlinks ($$) {
my ($file, $path) = @_;
if ($file !~ m%^/%) { $file = "$path/$file"; }
my @fn = ($file);
while ($file = NextSymlink($file)) {
push @fn, $file;
}
return @fn;
}