#!perl
use strict;
use warnings;
use File::Spec;
use File::Basename qw/dirname/;
use Getopt::Long qw/GetOptions :config bundling/;
use Pod::Usage qw/pod2usage/;
use Config;
use ExtUtils::MakeMaker;
use LWP::Simple;
use YAML;
use CPAN::DistnameInfo;
use Module::CoreList;
use version;
use App::pmuninstall;
my $perl_version = version->new($])->numify;
my $base_url = 'http://deps.cpantesters.org/depended-on-by.pl?dist=';
my $cpanmetadb = 'http://cpanmetadb.appspot.com/v1.0/package';
my $uninstalled = 0;
my @inc = @INC;
my @core_modules_dir = do { my %h; grep !$h{$_}++, @Config{qw/archlib archlibexp privlib privlibexp/} };
my $opt = +{
check_deps => 1,
};
GetOptions(
'f|force' => \$opt->{force},
'v|verbose!' => \$opt->{verbose},
'c|checkdeps!' => \$opt->{check_deps},
'n|no-checkdeps!' => sub { $opt->{check_deps} = 0 },
'h|help!' => \$opt->{help},
'V|version!' => \$opt->{version},
'l|local-lib=s' => \$opt->{local_lib},
'L|local-lib-contained=s' => sub {
$opt->{local_lib} = $_[1];
$opt->{self_contained} = 1;
},
);
pod2usage 1 if $opt->{help};
if ($opt->{version}) {
warn "App::pmuninstall v$App::pmuninstall::VERSION\n";
exit;
}
pod2usage 1 if !@ARGV;
main(@ARGV);
exit;
sub main {
my @modules = @_;
if ($opt->{local_lib}) {
setup_local_lib($opt->{local_lib}, $opt->{self_contained});
}
for my $module (@modules) {
my($packlist, $dist, $vname) = find_packlist($module);
unless ($dist) {
warn "$module is not found.\n";
next;
}
unless ($packlist) {
warn "$module is not installed.\n";
next;
}
$packlist = File::Spec->catfile($packlist);
if (is_core_module($module, $packlist)) {
warn "$module is Core Module!! Can't be uninstall.\n";
next;
}
if ($opt->{force} or ask_permission($module, $dist, $vname, $packlist, $opt->{local_lib})) {
if (uninstall_from_packlist($packlist, $opt->{local_lib})) {
warn "$module is successfully uninstalled.\n\n";
$uninstalled++;
}
else {
warn "! $module is failed uninstall.\n";
}
}
}
if ($uninstalled) {
warn "You may want to rebuild man(1) entires. Try `mandb -c` if needed\n"
}
}
sub vname_for {
my $module = shift;
my $yaml = get("$cpanmetadb/$module") or return;
my $meta = YAML::Load($yaml);
my $info = CPAN::DistnameInfo->new($meta->{distfile}) or return;
return $info->distvname;
}
sub ask_permission {
my($module, $dist, $vname, $packlist, $local_lib_base) = @_;
my(@deps, %seen);
if ($opt->{check_deps}) {
$vname ||= vname_for($module) || $module;
warn "Checking modules depending on $vname\n" if $opt->{verbose};
warn "-> Getting from $base_url$vname\n" if $opt->{verbose};
my $content = get("$base_url$vname") || '';
for my $dep ($content =~ m|
]+>([a-zA-Z0-9_:-]+)|smg) {
$dep =~ s/^\s+|\s+$//smg; # trim
next if $seen{$dep}++;
push @deps, $dep if locate_pack($dep);
}
}
warn "$module is included in the distribution $dist and contains:\n\n";
for (fixup_packilist($packlist, $local_lib_base)) {
warn " $_";
}
warn "\n";
my $default = 'y';
if (@deps) {
warn "Also, they're depended on by the following dists you have:\n\n";
for my $dep (@deps) {
warn " $dep\n";
}
warn "\n";
$default = 'n';
}
lc(prompt("Are you sure to uninstall $dist?", $default)) eq 'y';
}
sub find_packlist {
my $module = shift;
warn "Finding $module in your \@INC\n" if $opt->{verbose};
# find with the given name first
(my $try_dist = $module) =~ s!::!-!g;
my $pl = locate_pack($try_dist);
return ($pl, $try_dist) if $pl;
warn "Looking up $module on cpanmetadb\n" if $opt->{verbose};
# map module -> dist and retry
my $yaml = get("$cpanmetadb/$module") or return;
my $meta = YAML::Load($yaml);
my $info = CPAN::DistnameInfo->new($meta->{distfile});
my $pl2 = locate_pack($info->dist);
return ($pl2, $info->dist, $info->distvname);
}
sub is_core_module {
my ($dist, $packlist) = @_;
return unless exists $Module::CoreList::version{$perl_version}{$dist};
my $is_core = 0;
for my $dir (@core_modules_dir) {
if ($packlist =~ /^$dir/) {
$is_core = 1;
last;
}
}
return $is_core;
}
sub locate_pack {
my $dist = shift;
$dist =~ s!-!/!g;
for my $lib (@inc) {
my $packlist = "$lib/auto/$dist/.packlist";
return $packlist if -f $packlist && -r _;
}
return;
}
sub uninstall_from_packlist {
my ($packlist, $local_lib_base) = @_;
my $inc = {
map { File::Spec->catfile($_) => 1 } @inc
};
my $failed;
for my $file (fixup_packilist($packlist, $local_lib_base)) {
chomp $file;
print -f $file ? 'unlink ' : 'not found', " : $file\n" if $opt->{verbose};
unlink $file or warn "$file: $!\n" and $failed++;
rm_empty_dir_from_file($file, $inc);
}
print "unlink : $packlist\n" if $opt->{verbose};
unlink $packlist or warn "$packlist: $!\n" and $failed++;
rm_empty_dir_from_file($packlist, $inc);
print "\n" if $opt->{verbose};
return !$failed;
}
sub fixup_packilist {
my ($packlist, $local_lib_base) = @_;
my @target_list;
my $is_local_lib = is_local_lib($packlist, $local_lib_base);
open my $in, "<", $packlist or die "$packlist: $!";
while (my $file = <$in>) {
if ($is_local_lib) {
next unless is_local_lib($file, $local_lib_base);
}
push @target_list, $file;
}
return @target_list;
}
sub is_local_lib {
my ($file, $local_lib_base) = @_;
return 0 unless exists $INC{'local/lib.pm'};
$local_lib_base ||= '~/perl5';
$local_lib_base = quotemeta File::Spec->catfile(Cwd::realpath($local_lib_base));
$file = File::Spec->catfile($file);
return $file =~ /^$local_lib_base/ ? 1 : 0;
}
sub is_empty_dir {
my ($dir) = @_;
opendir my $dh, $dir or die "$dir: $!";
my @dir = grep !/^\.{1,2}$/, readdir $dh;
closedir $dh;
return @dir ? 0 : 1;
}
sub rm_empty_dir_from_file {
my ($file, $inc) = @_;
my $dir = dirname $file;
return unless -d $dir;
return if $inc->{+File::Spec->catfile($dir)};
my $failed;
if (is_empty_dir($dir)) {
print "rmdir : $dir\n" if $opt->{verbose};
rmdir $dir or warn "$dir: $!\n" and $failed++;
rm_empty_dir_from_file($dir, $inc);
}
return !$failed;
}
# taken from cpan-outdated
sub setup_local_lib {
my ($base, $self_contained) = @_;
$base ||= '~/perl5/';
require local::lib;
local $SIG{__WARN__} = sub { }; # catch 'Attempting to write ...'
@inc = map { Cwd::realpath($_) } split $Config{path_sep},
+{local::lib->build_environment_vars_for($base, $self_contained ? 0 : 1)}->{PERL5LIB};
push @inc, @INC unless $self_contained;
}
__END__
=head1 NAME
pm-uninstall - Uninstall modules
=head1 SYNOPSIS
pm-uninstall [options] Module ...
options:
-v,--verbose Turns on chatty output
-f,--force Uninstalls without prompts
-c,--checkdeps Check dependencies ( default on )
-n,--no-checkdeps Not check dependencies
-h,--help This help message
-V,--version Show version
-l,--local-lib Additional module path
-L,--local-lib-contained Additional module path (don't include non-core modules)
=cut