#!perl use strict; use warnings; use ExtUtils::Installed; use Getopt::Long; use Config; use version; use IO::Zlib; use CPAN::DistnameInfo; use Module::Metadata; use URI; our $VERSION = "0.32"; my $mirror = 'http://www.cpan.org/'; my $local_lib; my $self_contained = 0; my $index_file; my $help; Getopt::Long::Configure("bundling"); Getopt::Long::GetOptions( 'h|help' => \$help, 'verbose' => \my $verbose, 'm|mirror=s' => \$mirror, 'index=s' => \$index_file, 'p|print-package' => \my $print_package, 'I=s' => sub { die "this option was deprecated" }, 'l|local-lib=s' => \$local_lib, 'L|local-lib-contained=s' => sub { $local_lib = $_[1]; $self_contained = 1; }, 'compare-changes' => sub { die "--compare-changes option was deprecated.\n" . "You can use 'cpan-listchanges `cpan-outdated -p`' instead.\n" . "cpanm cpan-listchanges # install from CPAN\n" }, 'exclude-core' => \my $exclude_core, ) or $help++; if ($help) { require Pod::Usage; Pod::Usage::pod2usage(); } $mirror =~ s:/$::; my $index_url = "${mirror}/modules/02packages.details.txt.gz"; $index_url = URI->new($index_url); if ($index_url->isa('URI::file')) { die '--index is incompatible with a file:// mirror' if defined $index_file; $index_file = $index_url->file } my $core_modules; if ($exclude_core) { require Module::CoreList; no warnings 'once'; $core_modules = $Module::CoreList::version{$]}; } unless ($ENV{HARNESS_ACTIVE}) { &main; exit; } sub modules_to_check { my @inc = @_; my @modules = ExtUtils::Installed->new(skip_cwd => 1, inc_override => \@inc)->modules; # As core modules may not have been listed by EUI because they lack # .packlist, we add them from Module::CoreList if (!$exclude_core || ($local_lib && !$self_contained)) { require Module::CoreList; # This adds duplicates, but they are removed by the caller push @modules, keys %{ $Module::CoreList::version{$]} }; } (@modules) } sub installed_version_for { my($pkg, $inc) = @_; local $SIG{__WARN__} = sub {}; my $meta = Module::Metadata->new_from_module($pkg, inc => $inc); $meta ? $meta->version($pkg) : undef; } sub main { my @inc = make_inc($local_lib, $self_contained); if ( !defined($index_file) || ! -e $index_file || -z $index_file || !$index_url->isa('URI::file')) { $index_file = get_index($index_url, $index_file) } my %installed = map { $_ => 1 } modules_to_check(@inc); my $fh = zopen($index_file) or die "cannot open $index_file"; # skip header part while (my $line = <$fh>) { last if $line eq "\n"; } # body part my %seen; my %dist_latest_version; LINES: while (my $line = <$fh>) { my ($pkg, $version, $dist) = split /\s+/, $line; next unless $installed{$pkg}; next if $version eq 'undef'; # The note below about the latest version heuristics applies here too next if $seen{$dist}; # $Mail::SpamAssassin::Conf::VERSION is 'bogus' # https://rt.cpan.org/Public/Bug/Display.html?id=73465 next unless $version =~ /[0-9]/; # if excluding core modules next if $exclude_core && exists $core_modules->{$pkg}; next if $dist =~ m{/perl-[0-9._]+\.tar\.(gz|bz2)$}; my $inst_version = installed_version_for($pkg, \@inc) or next; if (compare_version($inst_version, $version)) { $seen{$dist}++; if ($verbose) { printf "%-30s %-7s %-7s %s\n", $pkg, $inst_version, $version, $dist; } elsif ($print_package) { print "$pkg\n"; } else { print "$dist\n"; } } } } # return true if $inst_version is less than $version sub compare_version { my ($inst_version, $version) = @_; return 0 if $inst_version eq $version; my $inst_version_obj = eval { version->new($inst_version) } || version->new(permissive_filter($inst_version)); my $version_obj = eval { version->new($version) } || version->new(permissive_filter($version)); return $inst_version_obj < $version_obj ? 1 : 0; } # for broken packages. sub permissive_filter { local $_ = $_[0]; s/^[Vv](\d)/$1/; # Bioinf V2.0 s/^(\d+)_(\d+)$/$1.$2/; # VMS-IndexedFile 0_02 s/-[a-zA-Z]+$//; # Math-Polygon-Tree 0.035-withoutworldwriteables s/([a-j])/ord($1)-ord('a')/gie; # DBD-Solid 0.20a s/[_h-z-]/./gi; # makepp 1.50.2vs.070506 s/\.{2,}/./g; $_; } # Return the $fname (a generated File::Temp object if not provided) sub get_index { my ($url, $fname) = @_; require HTTP::Tiny; my $ua = HTTP::Tiny->new; my $response; if (defined $fname) { # If the file is not empty, use it as a local cached copy if (-s $fname) { $response = $ua->mirror($url, $fname); } else { # If the file is empty we do not trust its timestamp # so set a custom If-Modified-Since (Perl 5.0 release) $response = $ua->mirror($url, $fname, { headers => { 'if-modified-since' => 'Wed, 19 Oct 1994 17:18:57 GMT', }, }); } } else { require File::Temp; $fname = File::Temp->new(UNLINK => 1, SUFFIX => '.gz'); binmode $fname; $response = $ua->request( 'GET' => $url, { data_callback => sub { print {$fname} $_[0] }, } ); close $fname; } if ($response->{status} == 599) { die "Cannot get_index $url to $fname: $response->{content}"; # 304 = "Not Modified" is still a success since we are mirroring } elsif (! $response->{success}) { die "Cannot get_index $url to $fname: $response->{status} $response->{reason}"; } #print "$fname $response->{status} $response->{reason}\n"; # Return the filename (which might be a File::Temp object) $fname } sub zopen { # Explicitely stringify the filename as it may be a File::Temp object IO::Zlib->new("$_[0]", "rb"); } sub make_inc { my ($base, $self_contained) = @_; if ($base) { require local::lib; my @modified_inc = ( local::lib->install_base_perl_path($base), local::lib->install_base_arch_path($base), ); if ($self_contained) { push @modified_inc, @Config{qw(privlibexp archlibexp)}; } else { push @modified_inc, @INC; } return @modified_inc; } else { return @INC; } } __END__ =head1 NAME cpan-outdated - detect outdated CPAN modules in your environment =head1 SYNOPSIS # print a list of distributions that contain outdated modules % cpan-outdated # print a list of outdated modules in packages % cpan-outdated -p # verbose % cpan-outdated --verbose  # ignore core modules (do not update dual life modules) % cpan-outdated --exclude-core # alternate mirrors % cpan-outdated --mirror file:///home/user/minicpan/ # additional module path(same as cpanminus) % cpan-outdated -l extlib/ % cpan-outdated -L extlib/ # install with cpan % cpan-outdated | xargs cpan -i # install with cpanm % cpan-outdated | cpanm % cpan-outdated -p | cpanm =head1 DESCRIPTION This script prints a list of outdated CPAN modules on your machine. This is the same feature as 'CPAN::Shell->r', but C is much faster and uses less memory. This script can be integrated with the L command. =head1 PRINTING PACKAGES VS DISTRIBUTIONS This script by default prints the outdated distribution as in the CPAN distro format, i.e: C so you can pipe it into CPAN installers, but with the C<-p> option it can be tweaked to print the module's package names. If you wish to manage a set of modules separately from your system perl installation and not install newer versions of "dual life modules" that are distributed with perl, the C<--exclude-core> option will make cpan-outdated ignore changes to core modules. Used with tools like cpanm and its C<-L --local-lib-contained> and C<--self-contained> options, this facilitates maintaining updates on standalone sets of modules. For some tools, such as L, installing from packages could be a bit more useful since you can track to see the old version number which you upgrade from. =head1 AUTHOR Tokuhiro Matsuno =head1 LICENSE Copyright (C) 2009 Tokuhiro Matsuno. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L L If you want to see what's changed for modules that require upgrades, use L =cut