File Coverage

blib/lib/App/pmuninstall.pm
Criterion Covered Total %
statement 42 270 15.5
branch 0 156 0.0
condition 0 38 0.0
subroutine 14 45 31.1
pod 0 26 0.0
total 56 535 10.4


line stmt bran cond sub pod time code
1             package App::pmuninstall;
2 1     1   56352 use strict;
  1         2  
  1         24  
3 1     1   4 use warnings;
  1         1  
  1         18  
4 1     1   4 use File::Spec;
  1         2  
  1         20  
5 1     1   5 use File::Basename qw(dirname);
  1         1  
  1         73  
6 1     1   445 use ExtUtils::Packlist;
  1         1449  
  1         27  
7 1     1   602 use Getopt::Long qw(GetOptions :config bundling);
  1         10958  
  1         7  
8 1     1   202 use Config;
  1         2  
  1         30  
9 1     1   368 use YAML ();
  1         5695  
  1         21  
10 1     1   388 use CPAN::DistnameInfo;
  1         825  
  1         25  
11 1     1   364 use version;
  1         1516  
  1         5  
12 1     1   689 use HTTP::Tiny;
  1         41647  
  1         37  
13 1     1   538 use Term::ANSIColor qw(colored);
  1         6881  
  1         1405  
14 1     1   8 use Cwd ();
  1         1  
  1         17  
15 1     1   613 use JSON::PP qw(decode_json);
  1         11294  
  1         2875  
16              
17             our $VERSION = "0.31";
18              
19             my $perl_version = version->new($])->numify;
20             my $depended_on_by = 'http://deps.cpantesters.org/depended-on-by.pl?dist=';
21             my $cpanmetadb = 'http://cpanmetadb.plackperl.org/v1.0/package';
22             my @core_modules_dir = do { my %h; grep !$h{$_}++, @Config{qw/archlib archlibexp privlib privlibexp/} };
23              
24             $ENV{ANSI_COLORS_DISABLED} = 1 if $^O eq 'MSWin32';
25              
26             our $OUTPUT_INDENT_LEVEL = 0;
27              
28             sub new {
29 0     0 0   my ($class, $inc) = @_;
30 0 0         $inc = [@INC] unless ref $inc eq 'ARRAY';
31 0           bless {
32             check_deps => 1,
33             verbose => 0,
34             inc => $class->prepare_include_paths($inc),
35             }, $class;
36             }
37              
38             sub run {
39 0     0 0   my ($self, @args) = @_;
40 0           local @ARGV = @args;
41             GetOptions(
42             'f|force' => \$self->{force},
43 0     0     'v|verbose!' => sub { ++$self->{verbose} },
44             'c|checkdeps!' => \$self->{check_deps},
45 0     0     'n|no-checkdeps!' => sub { $self->{check_deps} = 0 },
46             'q|quiet!' => \$self->{quiet},
47 0     0     'h|help!' => sub { $self->usage },
48             'V|version!' => \$self->{version},
49             'l|local-lib=s' => \$self->{local_lib},
50             'L|local-lib-contained=s' => sub {
51 0     0     $self->{local_lib} = $_[1];
52 0           $self->{self_contained} = 1;
53             },
54 0 0         ) or $self->usage;
55              
56 0 0         if ($self->{version}) {
57 0           $self->puts("pm-uninstall (App::pmuninstall) version $App::pmuninstall::VERSION");
58 0           exit;
59             }
60              
61 0 0         $self->short_usage unless @ARGV;
62              
63 0           $self->uninstall(@ARGV);
64             }
65              
66             sub uninstall {
67 0     0 0   my ($self, @modules) = @_;
68              
69 0           $self->setup_local_lib;
70              
71 0           my $uninstalled = 0;
72 0           for my $module (@modules) {
73 0 0         $self->puts("--> Working on $module") unless $self->{quiet};
74 0           my ($packlist, $dist, $vname) = $self->find_packlist($module);
75              
76 0           $packlist = File::Spec->canonpath($packlist);
77 0 0         if ($self->is_core_module($module, $packlist)) {
78 0           $self->puts(colored ['red'], "! $module is a core module!! Can't be uninstalled.");
79 0 0         $self->puts unless $self->{quiet};
80 0           next;
81             }
82              
83 0 0         unless ($dist) {
84 0           $self->puts(colored ['red'], "! $module not found.");
85 0 0         $self->puts unless $self->{quiet};
86 0           next;
87             }
88              
89 0 0         unless ($packlist) {
90 0           $self->puts(colored ['red'], "! $module is not installed.");
91 0 0         $self->puts unless $self->{quiet};
92 0           next;
93             }
94              
95 0 0         if ($self->ask_permission($module, $dist, $vname, $packlist)) {
96 0 0         if ($self->uninstall_from_packlist($packlist)) {
97 0           $self->puts(colored ['green'], "Successfully uninstalled $module");
98 0           ++$uninstalled;
99             }
100             else {
101 0           $self->puts(colored ['red'], "! Failed to uninstall $module");
102             }
103 0 0         $self->puts unless $self->{quiet};
104             }
105             }
106              
107 0 0         if ($uninstalled) {
108 0 0         $self->puts if $self->{quiet};
109 0           $self->puts("You may want to rebuild man(1) entries. Try `mandb -c` if needed");
110             }
111              
112 0           return $uninstalled;
113             }
114              
115             sub uninstall_from_packlist {
116 0     0 0   my ($self, $packlist) = @_;
117              
118             my $inc = {
119 0           map { File::Spec->canonpath($_) => 1 } @{$self->{inc}}
  0            
  0            
120             };
121              
122 0           my $failed;
123 0           for my $file ($self->fixup_packlist($packlist)) {
124 0           chomp $file;
125 0 0         $self->puts(-f $file ? 'unlink ' : 'not found', " : $file") if $self->{verbose};
    0          
126 0 0 0       unlink $file or $self->puts("$file: $!") and $failed++;
127 0           $self->rm_empty_dir_from_file($file, $inc);
128             }
129 0 0         $self->puts("unlink : $packlist") if $self->{verbose};
130 0 0 0       unlink $packlist or $self->puts("$packlist: $!") and $failed++;
131 0           $self->rm_empty_dir_from_file($packlist, $inc);
132              
133 0 0         if (my $install_json = $self->{install_json}) {
134 0 0         $self->puts("unlink : $install_json") if $self->{verbose};
135 0 0 0       unlink $install_json or $self->puts("$install_json: $!") and $failed++;
136 0           $self->rm_empty_dir_from_file($install_json);
137             }
138              
139 0 0 0       $self->puts unless $self->{quiet} || $self->{force};
140 0           return !$failed;
141             }
142              
143             sub rm_empty_dir_from_file {
144 0     0 0   my ($self, $file, $inc) = @_;
145 0           my $dir = dirname $file;
146 0 0         return unless -d $dir;
147 0 0         return if $inc->{+File::Spec->canonpath($dir)};
148              
149 0           my $failed;
150 0 0         if ($self->is_empty_dir($dir)) {
151 0 0         $self->puts("rmdir : $dir") if $self->{verbose};
152 0 0 0       rmdir $dir or $self->puts("$dir: $!") and $failed++;
153 0           $self->rm_empty_dir_from_file($dir, $inc);
154             }
155              
156 0           return !$failed;
157             }
158              
159             sub is_empty_dir {
160 0     0 0   my ($self, $dir) = @_;
161 0 0         opendir my $dh, $dir or die "$dir: $!";
162 0           my @dir = grep !/^\.{1,2}$/, readdir $dh;
163 0           closedir $dh;
164 0 0         return @dir ? 0 : 1;
165             }
166              
167             sub find_packlist {
168 0     0 0   my ($self, $module) = @_;
169 0 0         $self->puts("Finding $module in your \@INC") if $self->{verbose};
170              
171             # find with the given name first
172 0           (my $try_dist = $module) =~ s!::!-!g;
173 0 0         if (my $pl = $self->locate_pack($try_dist)) {
174 0 0         $self->puts("-> Found $pl") if $self->{verbose};
175 0           return ($pl, $try_dist);
176             }
177              
178 0 0         $self->puts("Looking up $module on cpanmetadb") if $self->{verbose};
179              
180             # map module -> dist and retry
181 0 0         my $yaml = $self->fetch("$cpanmetadb/$module") or return;
182 0           my $meta = YAML::Load($yaml);
183 0           my $info = CPAN::DistnameInfo->new($meta->{distfile});
184              
185 0   0       my $name = $self->find_meta($info->distvname) || $info->dist;
186 0 0         if (my $pl = $self->locate_pack($name)) {
187 0 0         $self->puts("-> Found $pl") if $self->{verbose};
188 0           return ($pl, $info->dist, $info->distvname);
189             }
190             }
191              
192             sub find_meta {
193 0     0 0   my ($self, $distvname) = @_;
194              
195 0           my $name;
196 0           for my $lib (@{$self->{inc}}) {
  0            
197 0 0         next unless $lib =~ /$Config{archname}/;
198 0           my $install_json = "$lib/.meta/$distvname/install.json";
199 0 0 0       next unless -f $install_json && -r _;
200 0           my $data = decode_json +$self->slurp($install_json);
201 0   0       $name = $data->{name} || next;
202 0 0         $self->puts("-> Found $install_json") if $self->{verbose};
203 0           $self->{meta} = $install_json;
204 0           last;
205             }
206 0           return $name;
207             }
208              
209             sub locate_pack {
210 0     0 0   my ($self, $dist) = @_;
211 0           $dist =~ s!-!/!g;
212 0           for my $lib (@{$self->{inc}}) {
  0            
213 0           my $packlist = "$lib/auto/$dist/.packlist";
214 0 0         $self->puts("-> Finding .packlist $packlist") if $self->{verbose} > 1;
215 0 0 0       return $packlist if -f $packlist && -r _;
216             }
217 0           return;
218             }
219              
220             sub is_core_module {
221 0     0 0   my ($self, $dist, $packlist) = @_;
222 0           require Module::CoreList;
223 0 0         return unless exists $Module::CoreList::version{$perl_version}{$dist};
224 0 0         return 1 unless $packlist;
225              
226 0           my $is_core = 0;
227 0           for my $dir (@core_modules_dir) {
228 0           my $safe_dir = quotemeta $dir; # workaround for MSWin32
229 0 0         if ($packlist =~ /^$safe_dir/) {
230 0           $is_core = 1;
231 0           last;
232             }
233             }
234 0           return $is_core;
235             }
236              
237             sub ask_permission {
238 0     0 0   my($self, $module, $dist, $vname, $packlist) = @_;
239              
240 0           my @deps = $self->find_deps($vname, $module);
241              
242 0 0         $self->puts if $self->{verbose};
243             $self->puts("$module is included in the distribution $dist and contains:\n")
244 0 0         unless $self->{quiet};
245 0           for my $file ($self->fixup_packlist($packlist)) {
246 0           chomp $file;
247 0 0         $self->puts(" $file") unless $self->{quiet};
248             }
249 0 0         $self->puts unless $self->{quiet};
250              
251 0 0         return 'force uninstall' if $self->{force};
252              
253 0           my $default = 'y';
254 0 0         if (@deps) {
255 0           $self->puts("Also, they're depended on by the following installed dists:\n");
256 0           for my $dep (@deps) {
257 0           $self->puts(" $dep");
258             }
259 0           $self->puts;
260 0           $default = 'n';
261             }
262              
263 0           return lc($self->prompt("Are you sure you want to uninstall $dist?", $default)) eq 'y';
264             }
265              
266             sub find_deps {
267 0     0 0   my ($self, $vname, $module) = @_;
268              
269 0 0 0       return unless $self->{check_deps} && !$self->{force};
270 0 0 0       $vname ||= $self->vname_for($module) or return;
271              
272 0 0         $self->puts("Checking modules depending on $vname") if $self->{verbose};
273 0 0         my $content = $self->fetch("$depended_on_by$vname") or return;
274              
275 0           my (@deps, %seen);
276 0           for my $dep ($content =~ m|
  • ]+>([a-zA-Z0-9_:-]+)|smg) {
  • 277 0           $dep =~ s/^\s+|\s+$//smg; # trim
    278 0 0         next if $seen{$dep}++;
    279 0           local $OUTPUT_INDENT_LEVEL = $OUTPUT_INDENT_LEVEL + 1;
    280 0 0         $self->puts("Finding $dep in your \@INC (dependencies)") if $self->{verbose};
    281 0 0         push @deps, $dep if $self->locate_pack($dep);
    282             }
    283              
    284 0           return @deps;
    285             }
    286              
    287             sub prompt {
    288 0     0 0   my ($self, $msg, $default) = @_;
    289 0           require ExtUtils::MakeMaker;
    290 0           ExtUtils::MakeMaker::prompt($msg, $default);
    291             }
    292              
    293             sub fixup_packlist {
    294 0     0 0   my ($self, $packlist) = @_;
    295 0           my @target_list;
    296 0           my $is_local_lib = $self->is_local_lib($packlist);
    297 0           my $plist = ExtUtils::Packlist->new($packlist);
    298 0           while (my $file = each %$plist) {
    299 0 0         if ($is_local_lib) {
    300 0 0         next unless $self->is_local_lib($file);
    301             }
    302 0           push @target_list, $file;
    303             }
    304 0           return @target_list;
    305             }
    306              
    307             sub is_local_lib {
    308 0     0 0   my ($self, $file) = @_;
    309 0 0         return unless $self->{local_lib};
    310              
    311 0           my $local_lib_base = quotemeta File::Spec->canonpath(Cwd::realpath($self->{local_lib}));
    312 0           $file = File::Spec->canonpath(Cwd::realpath($file));
    313              
    314 0 0         return $file =~ /^$local_lib_base/ ? 1 : 0;
    315             }
    316              
    317             sub vname_for {
    318 0     0 0   my ($self, $module) = @_;
    319              
    320 0 0         $self->puts("Fetching $module vname on cpanmetadb") if $self->{verbose};
    321 0 0         my $yaml = $self->fetch("$cpanmetadb/$module") or return;
    322 0           my $meta = YAML::Load($yaml);
    323 0 0         my $info = CPAN::DistnameInfo->new($meta->{distfile}) or return;
    324              
    325 0           return $info->distvname;
    326             }
    327              
    328             # taken from cpan-outdated
    329             sub setup_local_lib {
    330 0     0 0   my $self = shift;
    331 0 0         return unless $self->{local_lib};
    332              
    333 0 0         unless (-d $self->{local_lib}) {
    334 0           $self->puts(colored ['red'], "! $self->{local_lib} : no such directory");
    335 0           exit 1;
    336             }
    337              
    338 0     0     local $SIG{__WARN__} = sub { }; # catch 'Attempting to write ...'
    339             $self->{inc} = [
    340 0           grep { defined }
    341 0           map { Cwd::realpath($_) }
    342 0           @{$self->build_active_perl5lib($self->{local_lib}, $self->{self_contained})}
      0            
    343             ];
    344 0 0         push @{$self->{inc}}, @INC unless $self->{self_contained};
      0            
    345             }
    346              
    347             sub build_active_perl5lib {
    348 0     0 0   my ($self, $path, $interpolate) = @_;
    349             my $perl5libs = [
    350             $self->install_base_arch_path($path),
    351             $self->install_base_perl_path($path),
    352 0 0 0       $interpolate && $ENV{PERL5LIB} ? split(/\Q$Config{path_sep}\E/, $ENV{PERL5LIB}) : (),
    353             ];
    354 0           return $perl5libs;
    355             }
    356              
    357             sub install_base_perl_path {
    358 0     0 0   my ($self, $path) = @_;
    359 0           File::Spec->catdir($path, 'lib', 'perl5');
    360             }
    361              
    362             sub install_base_arch_path {
    363 0     0 0   my ($self, $path) = @_;
    364 0           File::Spec->catdir($self->install_base_perl_path($path), $Config{archname});
    365             }
    366              
    367             sub fetch {
    368 0     0 0   my ($self, $url) = @_;
    369 0 0         $self->puts("-> Fetching from $url") if $self->{verbose};
    370 0           my $res = HTTP::Tiny->new->get($url);
    371 0 0         return if $res->{status} == 404;
    372 0 0         die "[$res->{status}] fetch $url failed!!\n" if !$res->{success};
    373 0           return $res->{content};
    374             }
    375              
    376             sub slurp {
    377 0     0 0   my ($self, $file) = @_;
    378 0 0         open my $fh, '<', $file or die "$file $!";
    379 0           do { local $/; <$fh> };
      0            
      0            
    380             }
    381              
    382             sub puts {
    383 0     0 0   my ($self, @msg) = @_;
    384 0 0         push @msg, '' unless @msg;
    385 0 0         print ' ' x $OUTPUT_INDENT_LEVEL if $OUTPUT_INDENT_LEVEL;
    386 0           print @msg, "\n";
    387             }
    388              
    389             sub usage {
    390 0     0 0   my $self = shift;
    391 0           $self->puts(<< 'USAGE');
    392             Usage:
    393             pm-uninstall [options] Module [...]
    394              
    395             options:
    396             -v,--verbose Turns on chatty output
    397             -f,--force Uninstalls without prompts
    398             -c,--checkdeps Check dependencies (defaults to on)
    399             -n,--no-checkdeps Don't check dependencies
    400             -q,--quiet Suppress some messages
    401             -h,--help This help message
    402             -V,--version Show version
    403             -l,--local-lib Additional module path
    404             -L,--local-lib-contained Additional module path (don't include non-core modules)
    405             USAGE
    406              
    407 0           exit 1;
    408             }
    409              
    410             sub short_usage {
    411 0     0 0   my $self = shift;
    412 0           $self->puts(<< 'USAGE');
    413             Usage: pm-uninstall [options] Module [...]
    414              
    415             Try `pm-uninstall --help` or `man pm-uninstall` for more options.
    416             USAGE
    417              
    418 0           exit 1;
    419             }
    420              
    421             sub prepare_include_paths {
    422 0     0 0   my ($class, $inc) = @_;
    423 0           my $new_inc = [];
    424 0           my $archname = quotemeta $Config{archname};
    425 0           for my $path (@$inc) {
    426 0           push @$new_inc, $path;
    427 0 0 0       next if $path eq '.' or $path =~ /$archname/;
    428 0           push @$new_inc, File::Spec->catdir($path, $Config{archname});
    429             }
    430 0           return [do { my %h; grep !$h{$_}++, @$new_inc }];
      0            
      0            
    431             }
    432              
    433             1;
    434             __END__