#!/usr/bin/perl # # Copyright (c) 2003 Stefan Walter # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # Interactive script for deinstalling "leaf" packages # # Syntax: pkg_cutleaves [-cFgLlRx] # Options: # -c: Show comments, too; only works with '-l' (ignored otherwise) # -F: Fix package db after each deinstallation run (via 'pkgdb -F') # -g: Generate exclude list from kept/installed leaf packages # -L: Interpret exclude file as list of packages that *should* be installed # -l: List leaf packages only, don't ask if they should be deinstalled # -R: Autoprune new leaves # -x: Honor exclude list in $excludefile use Getopt::Std; use strict; my $dbdir = "/var/db/pkg"; my $excludefile = "/usr/local/etc/pkg_leaves.exclude"; my $pkgdeinstall = "/usr/sbin/pkg_delete"; my @pkgdb_args = ("/usr/local/sbin/pkgdb", "-F"); my $exclpattern; my %leavestokeep; my %opt; getopts('cFgLlRx', \%opt); set_excl_pattern(); # LIST MODE if ($opt{l}) { # Just print out the list of leaves, one per line my %leaves = get_leaves(); foreach my $leaf (sort keys %leaves) { if ($opt{c}) { print "$leaf - $leaves{$leaf}\n"; } else { print "$leaf\n"; } $leavestokeep{$leaf} = 1; } } # LINT MODE elsif ($opt{L}) { my @excludes = get_excludelist(); my @pkgs = get_packages(); # For each installed package for my $pkg (@pkgs) { my ($file, $required) = @$pkg; # Clobber any exclude patterns that match this package for (my $i = 0; $i < @excludes; $i++) { if ($file =~ /\Q@excludes[$i]\E/) { splice(@excludes, $i--, 1); } } # If matches exclude list and is not a leaf if ($required && ($file =~ $exclpattern)) { print "$file matches exclude list, but is not a leaf\n" } else { $leavestokeep{$file} = 1; } } # In exclude list, but not installed # For each remaining entry in exclude list foreach my $exclude (@excludes) { print "Exclude pattern '$exclude' matches no installed packages\n"; } } # INTERACTIVE MODE else { my @cutleaves; my ($i, $again); # Get list of leaf packages and put them into a hash my %leaves = get_leaves(); # Any leaves to work with? my $nleaves = keys %leaves; if ($nleaves > 0) { # If we don't have superuser rights, notify the user. if ($< != 0) { print "You need to have root permissions for deinstalling packages.\n"; $again = 'n'; } else { $again = 'y'; } } else { # If not, don't go on, there's nothing to do. print "** Didn't find any leaves to work with, exiting.\n"; print "** If this is unexpected, check your exclude file, please.\n"; $again = 'n'; } # Loop while the user wants to ROUND: while($again eq 'y') { # Always start with an empty list of leaves to cut my %leavestocut; # Initialize counter for progress status $i = 1; LEAVESLOOP: foreach my $leaf (sort keys %leaves) { print "Package $i of $nleaves:\n"; print "$leaf - $leaves{$leaf}\n"; print "$leaf - [keep]/(d)elete/(f)lush marked pkgs/(a)bort? "; # Get first character of input, without leading whitespace my ($answer) = (lc() =~ m/(\S)/); if ($answer eq 'd') { print "** Marking $leaf for removal.\n\n"; $leavestocut{$leaf} = 1; } elsif ($answer eq 'f') { print "\n"; last LEAVESLOOP; } elsif ($answer eq 'a') { print "\n"; $opt{aborted} = 1; last ROUND; } else { print "** Keeping $leaf.\n\n"; $leavestokeep{$leaf} = 1; } $i++; } # LEAVESLOOP AUTOPRUNE: # The -R switch jump # Initialize 'progress meter' my $ncuts = keys %leavestocut; my $noff = 0; # loop through packages marked for removal and pkg_deinstall them foreach my $leaf (sort keys %leavestocut) { $noff++; print "Deleting $leaf (package $noff of $ncuts).\n"; my @deinstall_args = ($pkgdeinstall, $leaf); if ((my $status = system(@deinstall_args) >> 8) != 0) { print STDERR "\n\n$0: pkg_deinstall returned $status - exiting, fix this first.\n\n"; last ROUND; } push @cutleaves, $leaf; } # Run 'pkgdb -F' if requested if ($opt{F}) { print "Running 'pkgdb -F'.\n"; if ((my $status = system(@pkgdb_args) >> 8) != 0) { print STDERR "\n\n$0: pkgdb returned $status - exiting, fix this first.\n\n"; last ROUND; } } # Get new list of leaf packages and put them into a hash %leaves = get_leaves(); # Ignore all leaves the user already told us to keep foreach my $leaf (keys %leavestokeep) { delete $leaves{$leaf} } # Any leaves left? $nleaves = keys %leaves; if ($nleaves == 0) { # If not, don't go on, there's nothing left to do. print "** Didn't find any new leaves to work with, exiting.\n"; last ROUND; } if ($opt{R}) { # start autopruning new leaves print "\n** Autopruning new leaves (Ctrl-C now to stop!) **\n" x 2; sleep 1; %leavestocut = %leaves; goto AUTOPRUNE; } # AUTOPRUNE print "Go on with new leaf packages ((y)es/[no])? "; # Get first character of input, without leading whitespace ($again) = (lc() =~ /(\S)/o); print "\n"; } # ROUND # print list of removed packages, sorted lexically, and their number print "** Deinstalled packages:\n"; foreach my $cutleaf (sort @cutleaves) { print "$cutleaf\n"; } my $noff = @cutleaves; print "** Number of deinstalled packages: $noff\n"; } # Generate exclude file if ($opt{g}) { if ($opt{aborted}) { die "\n** Skipping exclude file generation on aborted session **\n"; } if (-e $excludefile) { print "\nExclude file ($excludefile) exists! Overwrite ((y)es/[no])? "; my $answer = ; unless ($answer =~ /^y(es)?$/io) { exit 0; } } create_excludelist(); } # # Set the exclude pattern # sub set_excl_pattern { my @excludes = get_excludelist(); $exclpattern = @excludes ? join('|', map{qr(\Q$_\E)} @excludes) : ' '; # default non-exclusive $exclpattern = qr{^($exclpattern)}o; } # # Read the exclude list if the file exists # Parameter: path of the exclude file # sub get_excludelist { my @excludelist; # XXX: Don't check command line params in a subroutine if (($opt{x} || $opt{L}) && -f $excludefile && -T $excludefile) { open(EXCLFILE, $excludefile) or die "Couldn't open $excludefile!"; while(my $exclude = ) { chomp($exclude); # Ignore comments and empty lines, add others to the list unless ($exclude =~ /(^ *#)|(^ *$)/o) { push(@excludelist, $exclude); } } close(EXCLFILE) or warn "Failed to close exclude file ($excludefile): $!\n"; } return @excludelist; } # # Return a list of all packages # sub get_packages { my @pkgs; opendir(DBDIR, $dbdir) or die "Can't open package db directory $dbdir!"; while (defined(my $file = readdir(DBDIR))) { my $path = join('/', $dbdir, $file); unless ($file =~ /^\.+$/o || !(-d $path)) { push @pkgs, [$file, -s $path . '/+REQUIRED_BY', join('/', $path, '+COMMENT')]; } } closedir DBDIR; return @pkgs; } # # Get a hash (name => comment) of all leaves # sub get_leaves { my %leaves; my @pkgs = get_packages(); foreach my $pkg (@pkgs) { my ($file, $required, $commentfile) = @$pkg; unless ($required) { if ($file =~ $exclpattern) { $leavestokeep{$file} = 1; } else { # Read package's short description/comment my $comment; if ((-s $commentfile) && (open(COMMENT, $commentfile))) { chomp($comment = ); } else { $comment = 'No short description'; } $leaves{$file} = $comment; } } } return %leaves; } # # Write the list of exclusions to a file # sub create_excludelist { open(EXCLFILE, ">$excludefile") or die "Failed to open exclude list ($excludefile): $!\n"; print EXCLFILE '# Auto-generated exclude list ', scalar localtime, "\n"; for (sort keys %leavestokeep) { /^(.+)-\d.+$/o ? print EXCLFILE $1, "\n" : warn 'Unable to extract exclude pattern from ', $_, "\n"; } print "New exclude list ($excludefile) generated.\n"; }