#!/usr/bin/perl # ex:ts=8 sw=4: # $OpenBSD: pkg_delete,v 1.48 2004/09/24 09:06:50 espie Exp $ # # Copyright (c) 2003-2004 Marc Espie # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. use strict; use warnings; use Getopt::Std; use OpenBSD::PackingList; use OpenBSD::PackingOld; use OpenBSD::PackageInfo; use OpenBSD::RequiredBy; use OpenBSD::Logger; use OpenBSD::Vstat; use OpenBSD::PackageInfo; use OpenBSD::Error; our %forced = (); sub ensure_ldconfig { my $verbose = shift; return unless defined $OpenBSD::PackingElement::Lib::todo; VSystem($verbose, @OpenBSD::PackingElement::Lib::ldconfig, "-R"); undef $OpenBSD::PackingElement::Lib::todo; } sub record_all_shared_items { my $db = {dirs=>{}, users=>{}, groups=>{}}; my @list = installed_packages(); for my $e (@list) { my $plist = OpenBSD::PackingList->fromfile(installed_info($e).CONTENTS, \&OpenBSD::PackingList::DirrmOnly) or next; for my $item (@{$plist->{users}}, @{$plist->{groups}}, @{$plist->{items}}) { $item->record_shared_item($e, $db); } } return $db; } sub cleanup_shared_items { my $state = shift; my $h = $state->{dirs_to_rm}; my $u = $state->{users_to_rm}; my $g = $state->{groups_to_rm}; return unless defined $h or defined $u or defined $g; my $remaining = record_all_shared_items(); if (defined $h) { for my $d (sort {$b cmp $a} keys %$h) { my $realname = $state->{destdir}.$d; if ($remaining->{dirs}->{$realname}) { for my $i (@{$h->{$d}}) { $i->reload($state); } } else { for my $i (@{$h->{$d}}) { $i->cleanup($state); } if (!rmdir $realname) { print "Error deleting directory $realname: $!\n"; } } } } if (defined $u) { for my $user (keys %$u) { next if $remaining->{users}->{$user}; System("/usr/sbin/userdel", $user); } } if (defined $g) { for my $group (keys %$g) { next if $remaining->{groups}->{$group}; System("/usr/sbin/groupdel", $group); } } } package OpenBSD::PackingElement; sub delete { } sub record_shared_item { } sub cleanup { } sub reload { } sub log_pkgname { my ($self, $state) = @_; if (defined $state->{pkgname_tolog}) { OpenBSD::Logger::log "# package ", $state->{pkgname_tolog}, "\n"; $state->{pkgname_tolog} = undef; } } package OpenBSD::PackingElement::NewUser; sub record_shared_item { my ($self, $pkgname, $db) = @_; my $k = $self->{name}; $db->{users}->{$k} = 1; } sub delete { my ($self, $state) = @_; my $name = $self->{name}; if ($state->{verbose} or $state->{not}) { print "rmuser: $name\n"; } $state->{users_to_rm} = {} unless defined $state->{users_to_rm}; my $h = $state->{users_to_rm}; $h->{$name} = 1; } package OpenBSD::PackingElement::NewGroup; sub record_shared_item { my ($self, $pkgname, $db) = @_; my $k = $self->{name}; $db->{groups}->{$k} = 1; } sub delete { my ($self, $state) = @_; my $name = $self->{name}; if ($state->{verbose} or $state->{not}) { print "rmgroup: $name\n"; } $state->{groups_to_rm} = {} unless defined $state->{groups_to_rm}; my $h = $state->{groups_to_rm}; $h->{$name} = 1; } package OpenBSD::PackingElement::DirBase; sub record_shared_item { my ($self, $pkgname, $db) = @_; my $k = $self->fullname(); $db->{dirs}->{$k} = 1; } sub delete { my ($self, $state) = @_; my $name = $self->fullname(); if ($state->{verbose} or $state->{not}) { print "dirrm: $name\n"; } $state->{dirs_to_rm} = {} unless defined $state->{dirs_to_rm}; my $h = $state->{dirs_to_rm}; $h->{$name} = [] unless defined $h->{$name}; push(@{$h->{$name}}, $self->clone()); } package OpenBSD::PackingElement::DirRm; sub record_shared_item { &OpenBSD::PackingElement::DirBase::record_shared_item; } sub delete { &OpenBSD::PackingElement::DirBase::delete; } package OpenBSD::PackingElement::Unexec; use OpenBSD::Error; sub delete { my ($self, $state) = @_; my $cmd = $self->{expanded}; main::ensure_ldconfig($state->{verbose}) unless $state->{not}; if ($state->{verbose} or $state->{not}) { print "unexec: $cmd\n"; } return if $state->{not}; System('/bin/sh', '-c', $cmd); } package OpenBSD::PackingElement::FileBase; use OpenBSD::md5; sub delete { my ($self, $state) = @_; my $name = $self->fullname(); my $realname = $state->{destdir}.$name; if (-l $realname) { if ($state->{verbose} or $state->{not}) { print "deleting symlink: $realname\n"; } } else { if (! -f $realname) { print "File $realname does not exist\n"; return; } unless (defined($self->{link}) or $self->{nochecksum} or $state->{quick}) { if (!defined $self->{md5}) { print "Problem: $name does not have an md5 checksum\n"; print "NOT deleting: $realname\n"; $self->log_pkgname($state); OpenBSD::Logger::log "rm $state->{destdirname}$name\n"; return; } my $md5 = OpenBSD::md5::fromfile($realname); if ($md5 ne $self->{md5}) { print "Problem: md5 doesn't match for $name\n"; print "NOT deleting: $realname\n"; $self->log_pkgname($state); OpenBSD::Logger::log "rm $state->{destdirname}$name #MD5\n"; return; } } if ($state->{verbose} or $state->{not}) { print "deleting: $realname\n"; } } return if $state->{not}; if (!unlink $realname) { print "Problem deleting $realname\n"; $self->log_pkgname($state); OpenBSD::Logger::log "rm $state->{destdirname}$name\n"; } } package OpenBSD::PackingElement::Sample; use OpenBSD::md5; sub delete { my ($self, $state) = @_; my $name = $self->fullname(); my $realname = $state->{destdir}.$name; my $orig = $self->{copyfrom}; if (!defined $orig) { die "\@sample element does not reference a valid file\n"; } my $origname = $state->{destdir}.$orig->fullname(); if (! -e $realname) { print "File $realname does not exist\n"; return; } if (! -f $realname) { print "File $realname is not a file\n"; return; } if (!defined $orig->{md5}) { print "Problem: file $name does not have an md5 checksum\n"; print "NOT deleting: $realname\n"; $self->log_pkgname($state); OpenBSD::Logger::log "rm $state->{destdirname}$name\n"; return; } if ($state->{quick}) { unless ($state->{extra}) { print "NOT'deleting file $realname\n"; return; } } else { my $md5 = OpenBSD::md5::fromfile($realname); if ($md5 eq $orig->{md5}) { print "File $realname identical to sample\n"; } else { print "File $realname NOT identical to sample\n"; unless ($state->{extra}) { print "NOT deleting $realname\n"; return; } } } return if $state->{not}; print "deleting $realname\n"; if (!unlink $realname) { print "Problem deleting $realname\n"; $self->log_pkgname($state); OpenBSD::Logger::log "rm $state->{destdirname}$name\n"; } } package OpenBSD::PackingElement::InfoFile; use File::Basename; use OpenBSD::Error; sub delete { my ($self, $state) = @_; unless ($state->{not}) { my $fullname = $state->{destdir}.$self->fullname(); VSystem($state->{verbose}, "install-info", "--delete", "--info-dir=".dirname($fullname), $fullname); } $self->SUPER::delete($state); } package OpenBSD::PackingElement::Shell; sub delete { my ($self, $state) = @_; unless ($state->{not}) { my $destdir = $state->{destdir}; my $fullname = $self->fullname(); my @l=(); if (open(my $shells, '<', $destdir.'/etc/shells')) { local $_; while(<$shells>) { push(@l, $_); s/^\#.*//; if ($_ =~ m/^\Q$fullname\E\s*$/) { pop(@l); } } close($shells); open(my $shells2, '>', $destdir.'/etc/shells'); print $shells2 @l; close $shells2; print "Shell $fullname removed from $destdir/etc/shells\n"; } } $self->SUPER::delete($state); } package OpenBSD::PackingElement::Extra; sub delete { my ($self, $state) = @_; return unless $state->{extra}; my $name = $self->fullname(); my $realname = $state->{destdir}.$name; if ($state->{verbose} or $state->{not}) { print "deleting extra file: $realname\n"; } return if $state->{not}; return unless -e $realname; unlink($realname) or print "problem deleting extra file $realname\n"; } package OpenBSD::PackingElement::Extradir; sub delete { my ($self, $state) = @_; return unless $state->{extra}; return unless -e $state->{destdir}.$self->fullname(); $self->SUPER::delete($state); } package OpenBSD::PackingElement::Mandir; sub cleanup { my ($self, $state) = @_; my $fullname = $state->{destdir}.$self->fullname(); print "You may wish to remove ", $fullname, " from man.conf\n"; unlink("$fullname/whatis.db"); } package OpenBSD::PackingElement::Fontdir; sub cleanup { my ($self, $state) = @_; my $fullname = $state->{destdir}.$self->fullname(); print "You may wish to remove ", $fullname, " from your font path\n"; unlink("$fullname/fonts.alias"); unlink("$fullname/fonts.dir"); unlink("$fullname/fonts.cache-1"); } package OpenBSD::PackingElement::Infodir; sub cleanup { my ($self, $state) = @_; my $fullname = $state->{destdir}.$self->fullname(); unlink("$fullname/dir"); } package OpenBSD::PackingElement::ExtraUnexec; use OpenBSD::Error; sub delete { my ($self, $state) = @_; return unless $state->{extra}; my $cmd = $self->{expanded}; main::ensure_ldconfig($state->{verbose}) unless $state->{not}; if ($state->{verbose} or $state->{not}) { print "unexec: $cmd\n"; } return if $state->{not}; System($cmd); } package OpenBSD::PackingElement::Lib; sub delete { my ($self, $state) = @_; $self->SUPER::delete($state); $self->mark_ldconfig_directory($state->{destdir}); } package OpenBSD::PackingElement::FREQUIRE; use OpenBSD::PackageInfo; use OpenBSD::Error; sub delete { my ($self, $state) = @_; my $dir = $state->{dir}; my $opt_v = $state->{verbose}; my $opt_n = $state->{not}; my $pkgname = $state->{pkgname}; main::ensure_ldconfig($opt_v) unless $opt_n; print "Require script: $dir",REQUIRE," $pkgname DEINSTALL\n" if $opt_v or $opt_n; unless ($opt_n) { chmod 0755, $dir.REQUIRE; System($dir.REQUIRE, $pkgname, "DEINSTALL") == 0 or die "Require script borked"; } } package OpenBSD::PackingElement::FDEINSTALL; use OpenBSD::PackageInfo; use OpenBSD::Error; sub delete { my ($self, $state) = @_; my $dir = $state->{dir}; my $opt_v = $state->{verbose}; my $opt_n = $state->{not}; my $pkgname = $state->{pkgname}; main::ensure_ldconfig($opt_v) unless $opt_n; print "Deinstall script: $dir",DEINSTALL ," $pkgname DEINSTALL\n" if $opt_v or $opt_n; unless ($opt_n) { chmod 0755, $dir.DEINSTALL; System($dir.DEINSTALL, $pkgname, "DEINSTALL") == 0 or die "deinstall script borked"; } } package main; our ($opt_v, $opt_D, $opt_d, $opt_n, $opt_f, $opt_q, $opt_p, $opt_c, $opt_L, $opt_B); sub remove_packing_info { my $dir = shift; for my $fname (info_names()) { unlink($dir.$fname); } rmdir($dir) or die "Can't finish removing directory $dir: $!"; } sub manpages_unindex { my ($plist, $destdir) = @_; return unless defined $plist->{state}->{mandirs}; require OpenBSD::Makewhatis; while (my ($k, $v) = each %{$plist->{state}->{mandirs}}) { my @l = map { $destdir.$_ } @$v; eval { OpenBSD::Makewhatis::remove($destdir.$k, \@l); }; if ($@) { print STDERR "Error in makewhatis: $@\n"; } } } sub delete_package { my ($pkgname, $state) = @_; $state->{pkgname} = $pkgname; my $dir = installed_info($pkgname); $state->{dir} = $dir; my $plist = OpenBSD::PackingList->fromfile($dir.CONTENTS) or die "Bad package"; if (!defined $plist->pkgname()) { die "Package $pkgname has no name"; } if ($plist->pkgname() ne $pkgname) { die "Package $pkgname real name does not match"; } my $problems = 0; for my $item (@{$plist->{items}}) { next unless $item->IsFile(); my $fname = $item->fullname(); my $s = OpenBSD::Vstat::remove($fname, $item->{size}); next unless defined $s; if ($s->{ro}) { print "Error: ", $s->{mnt}, " is read-only ($fname)\n"; $problems++; } } die if $problems; $ENV{'PKG_PREFIX'} = $plist->pkgbase(); if ($plist->has(REQUIRE)) { $plist->get(REQUIRE)->delete($state); } if ($plist->has(DEINSTALL)) { $plist->get(DEINSTALL)->delete($state); } manpages_unindex($plist, $state->{destdir}); for my $item (@{$plist->{groups}}, @{$plist->{users}}, @{$plist->{items}}) { $item->delete($state); } # guard against duplicate pkgdep my $removed = {}; for my $item (@{$plist->{pkgdep}}) { my $name = $item->{name}; next if defined $removed->{$name}; print "remove dependency in $name\n" if $opt_v or $opt_n; local $@; eval { OpenBSD::RequiredBy->new($name)->delete($pkgname) unless $opt_n; }; if ($@) { print STDERR "$@\n"; } $removed->{$name} = 1; } remove_packing_info($dir) unless $opt_n; } getopts('vcDdnf:qpS:L:B:'); $opt_B = $ENV{'PKG_DESTDIR'} unless defined $opt_B; $opt_B = '' unless defined $opt_B; if ($opt_B ne '') { $opt_B.='/' unless $opt_B =~ m/\/$/; } $ENV{'PKG_DESTDIR'} = $opt_B; $opt_L = '/usr/local' unless defined $opt_L; if (defined $opt_p) { die "Option p is obsolete"; } if (defined $opt_d) { die "Option d is obsolete"; } if ($opt_f) { %forced = map {($_, 1)} split(/,/, $opt_f); } if ($< && !$forced{nonroot}) { die "$0 must be run as root"; } my %done; my $removed; my $state = {}; $state->{not} = $opt_n; $state->{quick} = $opt_q; $state->{verbose} = $opt_v; $state->{extra} = $opt_c; $ENV{'PKG_DELETE_EXTRA'} = $state->{extra} ? "Yes" : "No"; # First, resolve pkg names my @realnames; my $bad; for my $pkgname (@ARGV) { $pkgname =~ s/\.tgz$//; if (is_installed($pkgname)) { push(@realnames, installed_name($pkgname)); } else { if (OpenBSD::PackageName::is_stem($pkgname)) { my @l = OpenBSD::PackageName::findstem($pkgname, installed_packages()); if (@l == 0) { print "Can't resolve $pkgname to an installed package name\n"; $bad = 1 unless $forced{uninstalled}; } elsif (@l == 1) { push(@realnames, $l[0]); } elsif (@l != 0) { print "Ambiguous: $pkgname could be ", join(' ', @l),"\n"; if ($forced{ambiguous}) { print "(removing them all)\n"; push(@realnames, @l); } else { $bad = 1; } } } } } # Then check that dependencies are okay my (%toremove, %extra_rm); my @todo; for my $pkgname (@realnames) { $toremove{$pkgname} = 1; } push(@todo, @realnames); OpenBSD::Logger::log_as("pkg_delete"); while (my $pkgname = pop @todo) { my $deps = OpenBSD::RequiredBy->new($pkgname)->list(); if (@$deps > 0) { for my $dep (@$deps) { next if defined $toremove{$dep}; next if defined $extra_rm{$dep}; $extra_rm{$dep}=$pkgname; push(@todo, $dep); } } } if (keys(%extra_rm) != 0) { print "Can't remove ", join(' ', @ARGV), " without also removing:\n", join(' ', keys(%extra_rm)), "\n"; if ($forced{dependencies}) { print "(removing them as well)\n"; push(@realnames, keys(%extra_rm)); } else { $bad = 1; } } if ($bad) { exit(1); } $state->{destdir} = $opt_B; if ($opt_B eq '') { $state->{destdirname} = ''; } else { OpenBSD::Logger::annotate("PKG_DESTDIR=\"$opt_B\"; export PKG_DESTDIR\n"); $state->{destdirname} = '${PKG_DESTDIR}'; } eval { # and finally, handle the removal { do { $removed = 0; for my $pkgname (@realnames) { next if $done{$pkgname}; unless (is_installed($pkgname)) { print "$pkgname was not installed\n"; $done{$pkgname} = 1; $removed++; next; } my $deps = OpenBSD::RequiredBy->new($pkgname)->list(); next if @$deps > 0; print $opt_n ? "Pretending to delete " : "Deleting ", "$pkgname\n"; $state->{pkgname_tolog} = $pkgname; delete_package($pkgname, $state); delete_installed($pkgname); $done{$pkgname} = 1; $removed++; } # we're not actually doing anything, so we can't expect this loop # to ever finish last if $opt_n; } while ($removed); } }; my $dielater = $@; ensure_ldconfig($opt_v) unless $opt_n; # delayed directory/user/group removal cleanup_shared_items($state); OpenBSD::PackingElement::Fontdir::finish_fontdirs(); my $logname = OpenBSD::Logger::logname(); if (defined $logname) { print "Problems logged as $logname\n"; } if ($dielater) { die $@; }