File Coverage

blib/lib/App/pmuninstall.pm
Criterion Covered Total %
statement 39 267 14.6
branch 0 158 0.0
condition 0 38 0.0
subroutine 13 44 29.5
pod 0 26 0.0
total 52 533 9.7


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