File Coverage

blib/lib/App/pmuninstall.pm
Criterion Covered Total %
statement 42 276 15.2
branch 0 158 0.0
condition 0 38 0.0
subroutine 14 46 30.4
pod 0 26 0.0
total 56 544 10.2


line stmt bran cond sub pod time code
1             package App::pmuninstall;
2 1     1   55776 use strict;
  1         2  
  1         24  
3 1     1   4 use warnings;
  1         2  
  1         20  
4 1     1   4 use File::Spec;
  1         2  
  1         19  
5 1     1   4 use File::Basename qw(dirname);
  1         2  
  1         76  
6 1     1   404 use ExtUtils::Packlist;
  1         1512  
  1         29  
7 1     1   609 use Getopt::Long qw(GetOptions :config bundling);
  1         10538  
  1         8  
8 1     1   259 use Config;
  1         2  
  1         33  
9 1     1   351 use YAML ();
  1         5659  
  1         20  
10 1     1   386 use CPAN::DistnameInfo;
  1         804  
  1         24  
11 1     1   362 use version;
  1         1569  
  1         5  
12 1     1   678 use HTTP::Tiny;
  1         41425  
  1         39  
13 1     1   567 use Term::ANSIColor qw(colored);
  1         7236  
  1         1440  
14 1     1   7 use Cwd ();
  1         1  
  1         17  
15 1     1   632 use JSON::PP qw(decode_json);
  1         11243  
  1         2954  
16              
17             our $VERSION = "0.33";
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             # NOTE only use this for comparing paths
    308             sub _canon_path_compare {
    309 0     0     my ($self, $path) = @_;
    310 0           $path = Cwd::realpath($path);
    311 0 0         if( $^O eq 'MSWin32' ) {
    312 0           require Win32;
    313 0           $path = Win32::GetLongPathName($path);
    314             }
    315              
    316 0           return $path;
    317             }
    318              
    319             sub is_local_lib {
    320 0     0 0   my ($self, $file) = @_;
    321 0 0         return unless $self->{local_lib};
    322              
    323 0           my $local_lib_base = quotemeta $self->_canon_path_compare($self->{local_lib});
    324 0           $file = $self->_canon_path_compare($file);
    325              
    326 0 0         return $file =~ /^$local_lib_base(?:\/|\z)/ ? 1 : 0;
    327             }
    328              
    329             sub vname_for {
    330 0     0 0   my ($self, $module) = @_;
    331              
    332 0 0         $self->puts("Fetching $module vname on cpanmetadb") if $self->{verbose};
    333 0 0         my $yaml = $self->fetch("$cpanmetadb/$module") or return;
    334 0           my $meta = YAML::Load($yaml);
    335 0 0         my $info = CPAN::DistnameInfo->new($meta->{distfile}) or return;
    336              
    337 0           return $info->distvname;
    338             }
    339              
    340             # taken from cpan-outdated
    341             sub setup_local_lib {
    342 0     0 0   my $self = shift;
    343 0 0         return unless $self->{local_lib};
    344              
    345 0 0         unless (-d $self->{local_lib}) {
    346 0           $self->puts(colored ['red'], "! $self->{local_lib} : no such directory");
    347 0           exit 1;
    348             }
    349              
    350 0     0     local $SIG{__WARN__} = sub { }; # catch 'Attempting to write ...'
    351             $self->{inc} = [
    352 0           grep { defined }
    353 0           map { Cwd::realpath($_) }
    354 0           @{$self->build_active_perl5lib($self->{local_lib}, $self->{self_contained})}
      0            
    355             ];
    356 0 0         push @{$self->{inc}}, @INC unless $self->{self_contained};
      0            
    357             }
    358              
    359             sub build_active_perl5lib {
    360 0     0 0   my ($self, $path, $interpolate) = @_;
    361             my $perl5libs = [
    362             $self->install_base_arch_path($path),
    363             $self->install_base_perl_path($path),
    364 0 0 0       $interpolate && $ENV{PERL5LIB} ? split(/\Q$Config{path_sep}\E/, $ENV{PERL5LIB}) : (),
    365             ];
    366 0           return $perl5libs;
    367             }
    368              
    369             sub install_base_perl_path {
    370 0     0 0   my ($self, $path) = @_;
    371 0           File::Spec->catdir($path, 'lib', 'perl5');
    372             }
    373              
    374             sub install_base_arch_path {
    375 0     0 0   my ($self, $path) = @_;
    376 0           File::Spec->catdir($self->install_base_perl_path($path), $Config{archname});
    377             }
    378              
    379             sub fetch {
    380 0     0 0   my ($self, $url) = @_;
    381 0 0         $self->puts("-> Fetching from $url") if $self->{verbose};
    382 0           my $res = HTTP::Tiny->new->get($url);
    383 0 0         return if $res->{status} == 404;
    384 0 0         die "[$res->{status}] fetch $url failed!!\n" if !$res->{success};
    385 0           return $res->{content};
    386             }
    387              
    388             sub slurp {
    389 0     0 0   my ($self, $file) = @_;
    390 0 0         open my $fh, '<', $file or die "$file $!";
    391 0           do { local $/; <$fh> };
      0            
      0            
    392             }
    393              
    394             sub puts {
    395 0     0 0   my ($self, @msg) = @_;
    396 0 0         push @msg, '' unless @msg;
    397 0 0         print ' ' x $OUTPUT_INDENT_LEVEL if $OUTPUT_INDENT_LEVEL;
    398 0           print @msg, "\n";
    399             }
    400              
    401             sub usage {
    402 0     0 0   my $self = shift;
    403 0           $self->puts(<< 'USAGE');
    404             Usage:
    405             pm-uninstall [options] Module [...]
    406              
    407             options:
    408             -v,--verbose Turns on chatty output
    409             -f,--force Uninstalls without prompts
    410             -c,--checkdeps Check dependencies (defaults to on)
    411             -n,--no-checkdeps Don't check dependencies
    412             -q,--quiet Suppress some messages
    413             -h,--help This help message
    414             -V,--version Show version
    415             -l,--local-lib Additional module path
    416             -L,--local-lib-contained Additional module path (don't include non-core modules)
    417             USAGE
    418              
    419 0           exit 1;
    420             }
    421              
    422             sub short_usage {
    423 0     0 0   my $self = shift;
    424 0           $self->puts(<< 'USAGE');
    425             Usage: pm-uninstall [options] Module [...]
    426              
    427             Try `pm-uninstall --help` or `man pm-uninstall` for more options.
    428             USAGE
    429              
    430 0           exit 1;
    431             }
    432              
    433             sub prepare_include_paths {
    434 0     0 0   my ($class, $inc) = @_;
    435 0           my $new_inc = [];
    436 0           my $archname = quotemeta $Config{archname};
    437 0           for my $path (@$inc) {
    438 0           push @$new_inc, $path;
    439 0 0 0       next if $path eq '.' or $path =~ /$archname/;
    440 0           push @$new_inc, File::Spec->catdir($path, $Config{archname});
    441             }
    442 0           return [do { my %h; grep !$h{$_}++, @$new_inc }];
      0            
      0            
    443             }
    444              
    445             1;
    446             __END__