#!/usr/local/bin/perl
#
# Copyright (c) 2003 Stefan Walter <sw@gegenunendlich.de>
#
# 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(<STDIN>) =~ 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(<STDIN>) =~ /(\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 = <STDIN>;
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 = <EXCLFILE>) {
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 = <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";
}
syntax highlighted by Code2HTML, v. 0.9.1