summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRafael Garcia-Suarez <rgarciasuarez@gmail.com>2007-04-10 07:42:33 +0000
committerRafael Garcia-Suarez <rgarciasuarez@gmail.com>2007-04-10 07:42:33 +0000
commit6aaee015312976007994b80b72c65ab6b6237774 (patch)
treeda8e722757077a1fda3796b78f5db4bee5643249
parent8aefeee2c641e088fc7ae56870f0748e4f607f90 (diff)
downloadperl-6aaee015312976007994b80b72c65ab6b6237774.tar.gz
Add CPANPLUS 0.78
p4raw-id: //depot/perl@30883
-rw-r--r--MANIFEST97
-rwxr-xr-xinstallperl2
-rw-r--r--lib/CPANPLUS.pm271
-rw-r--r--lib/CPANPLUS/Backend.pm1061
-rw-r--r--lib/CPANPLUS/Backend/RV.pm144
-rw-r--r--lib/CPANPLUS/Config.pm264
-rw-r--r--lib/CPANPLUS/Configure.pm601
-rw-r--r--lib/CPANPLUS/Configure/Setup.pm1628
-rw-r--r--lib/CPANPLUS/Dist.pm505
-rw-r--r--lib/CPANPLUS/Dist/Base.pm249
-rw-r--r--lib/CPANPLUS/Dist/MM.pm955
-rw-r--r--lib/CPANPLUS/Dist/Sample.pm16
-rw-r--r--lib/CPANPLUS/Error.pm201
-rw-r--r--lib/CPANPLUS/FAQ.pod30
-rw-r--r--lib/CPANPLUS/Hacking.pod142
-rw-r--r--lib/CPANPLUS/Internals.pm489
-rw-r--r--lib/CPANPLUS/Internals/Constants.pm302
-rw-r--r--lib/CPANPLUS/Internals/Constants/Report.pm357
-rw-r--r--lib/CPANPLUS/Internals/Extract.pm236
-rw-r--r--lib/CPANPLUS/Internals/Fetch.pm372
-rw-r--r--lib/CPANPLUS/Internals/Report.pm609
-rw-r--r--lib/CPANPLUS/Internals/Search.pm316
-rw-r--r--lib/CPANPLUS/Internals/Source.pm1011
-rw-r--r--lib/CPANPLUS/Internals/Utils.pm536
-rw-r--r--lib/CPANPLUS/Internals/Utils/Autoflush.pm5
-rw-r--r--lib/CPANPLUS/Module.pm1580
-rw-r--r--lib/CPANPLUS/Module/Author.pm213
-rw-r--r--lib/CPANPLUS/Module/Author/Fake.pm80
-rw-r--r--lib/CPANPLUS/Module/Checksums.pm251
-rw-r--r--lib/CPANPLUS/Module/Fake.pm86
-rw-r--r--lib/CPANPLUS/Module/Signature.pm65
-rw-r--r--lib/CPANPLUS/Selfupdate.pm447
-rw-r--r--lib/CPANPLUS/Shell.pm314
-rw-r--r--lib/CPANPLUS/Shell/Classic.pm1236
-rw-r--r--lib/CPANPLUS/Shell/Default.pm1699
-rw-r--r--lib/CPANPLUS/Shell/Default/Plugins/HOWTO.pod136
-rw-r--r--lib/CPANPLUS/Shell/Default/Plugins/Remote.pm188
-rw-r--r--lib/CPANPLUS/Shell/Default/Plugins/Source.pm107
-rw-r--r--lib/CPANPLUS/bin/cpan2dist597
-rw-r--r--lib/CPANPLUS/bin/cpanp103
-rw-r--r--lib/CPANPLUS/bin/cpanp-run-perl10
-rw-r--r--lib/CPANPLUS/inc.pm522
-rw-r--r--lib/CPANPLUS/t/00_CPANPLUS-Inc.t190
-rw-r--r--lib/CPANPLUS/t/00_CPANPLUS-Internals-Utils.t132
-rw-r--r--lib/CPANPLUS/t/01_CPANPLUS-Configure.t142
-rw-r--r--lib/CPANPLUS/t/02_CPANPLUS-Internals.t123
-rw-r--r--lib/CPANPLUS/t/03_CPANPLUS-Internals-Source.t45
-rw-r--r--lib/CPANPLUS/t/04_CPANPLUS-Module.t302
-rw-r--r--lib/CPANPLUS/t/05_CPANPLUS-Internals-Fetch.t113
-rw-r--r--lib/CPANPLUS/t/06_CPANPLUS-Internals-Constants.t66
-rw-r--r--lib/CPANPLUS/t/07_CPANPLUS-Internals-Extract.t36
-rw-r--r--lib/CPANPLUS/t/08_CPANPLUS-Backend.t279
-rw-r--r--lib/CPANPLUS/t/09_CPANPLUS-Internals-Search.t78
-rw-r--r--lib/CPANPLUS/t/10_CPANPLUS-Error.t113
-rw-r--r--lib/CPANPLUS/t/19_CPANPLUS-Dist.t417
-rw-r--r--lib/CPANPLUS/t/20_CPANPLUS-Dist-MM.t403
-rw-r--r--lib/CPANPLUS/t/21_CPANPLUS-Dist-No-Build.t57
-rw-r--r--lib/CPANPLUS/t/30_CPANPLUS-Internals-Selfupdate.t122
-rw-r--r--lib/CPANPLUS/t/40_CPANPLUS-Internals-Report.t469
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz.packed19
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed34
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/CHECKSUMS30
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.readme2
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed39
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed18
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/CHECKSUMS20
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.readme2
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed51
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/CHECKSUMS20
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.readme2
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed35
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/CHECKSUMS20
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.readme2
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed50
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz.packed25
-rw-r--r--lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz.packed28
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz.packed19
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz.packed25
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz.packed28
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed34
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/CHECKSUMS30
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed39
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed18
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/CHECKSUMS20
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed51
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/CHECKSUMS20
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed35
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/CHECKSUMS20
-rw-r--r--lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed50
-rw-r--r--lib/CPANPLUS/t/inc/conf.pl173
-rw-r--r--utils.lst3
-rw-r--r--utils/Makefile12
-rw-r--r--utils/cpan2dist.PL51
-rw-r--r--utils/cpanp-run-perl.PL51
-rw-r--r--utils/cpanp.PL51
-rw-r--r--win32/Makefile5
-rw-r--r--win32/makefile.mk5
97 files changed, 22250 insertions, 7 deletions
diff --git a/MANIFEST b/MANIFEST
index 12caf9ea6d..adae648728 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1571,6 +1571,98 @@ lib/CPAN/Nox.pm Runs CPAN while avoiding compiled extensions
lib/CPAN/PAUSE2003.pub CPAN public key
lib/CPAN/PAUSE2005.pub CPAN public key
lib/CPAN/PAUSE2007.pub CPAN public key
+lib/CPANPLUS/Backend.pm CPANPLUS
+lib/CPANPLUS/Backend/RV.pm CPANPLUS
+lib/CPANPLUS/bin/cpan2dist the cpan2dist utility
+lib/CPANPLUS/bin/cpanp the cpanp utility
+lib/CPANPLUS/bin/cpanp-run-perl the cpanp-run-perl utility
+lib/CPANPLUS/Config.pm CPANPLUS
+lib/CPANPLUS/Configure.pm CPANPLUS
+lib/CPANPLUS/Configure/Setup.pm CPANPLUS
+lib/CPANPLUS/Dist/Base.pm CPANPLUS
+lib/CPANPLUS/Dist/MM.pm CPANPLUS
+lib/CPANPLUS/Dist.pm CPANPLUS
+lib/CPANPLUS/Dist/Sample.pm CPANPLUS
+lib/CPANPLUS/Error.pm CPANPLUS
+lib/CPANPLUS/FAQ.pod CPANPLUS
+lib/CPANPLUS/Hacking.pod CPANPLUS
+lib/CPANPLUS/inc.pm CPANPLUS
+lib/CPANPLUS/Internals/Constants.pm CPANPLUS
+lib/CPANPLUS/Internals/Constants/Report.pm CPANPLUS
+lib/CPANPLUS/Internals/Extract.pm CPANPLUS
+lib/CPANPLUS/Internals/Fetch.pm CPANPLUS
+lib/CPANPLUS/Internals.pm CPANPLUS
+lib/CPANPLUS/Internals/Report.pm CPANPLUS
+lib/CPANPLUS/Internals/Search.pm CPANPLUS
+lib/CPANPLUS/Internals/Source.pm CPANPLUS
+lib/CPANPLUS/Internals/Utils/Autoflush.pm CPANPLUS
+lib/CPANPLUS/Internals/Utils.pm CPANPLUS
+lib/CPANPLUS/Module/Author/Fake.pm CPANPLUS
+lib/CPANPLUS/Module/Author.pm CPANPLUS
+lib/CPANPLUS/Module/Checksums.pm CPANPLUS
+lib/CPANPLUS/Module/Fake.pm CPANPLUS
+lib/CPANPLUS/Module.pm CPANPLUS
+lib/CPANPLUS/Module/Signature.pm CPANPLUS
+lib/CPANPLUS.pm CPANPLUS
+lib/CPANPLUS/Selfupdate.pm CPANPLUS
+lib/CPANPLUS/Shell/Classic.pm CPANPLUS
+lib/CPANPLUS/Shell/Default/Plugins/HOWTO.pod CPANPLUS
+lib/CPANPLUS/Shell/Default/Plugins/Remote.pm CPANPLUS
+lib/CPANPLUS/Shell/Default/Plugins/Source.pm CPANPLUS
+lib/CPANPLUS/Shell/Default.pm CPANPLUS
+lib/CPANPLUS/Shell.pm CPANPLUS
+lib/CPANPLUS/t/00_CPANPLUS-Inc.t CPANPLUS tests
+lib/CPANPLUS/t/00_CPANPLUS-Internals-Utils.t CPANPLUS tests
+lib/CPANPLUS/t/01_CPANPLUS-Configure.t CPANPLUS tests
+lib/CPANPLUS/t/02_CPANPLUS-Internals.t CPANPLUS tests
+lib/CPANPLUS/t/03_CPANPLUS-Internals-Source.t CPANPLUS tests
+lib/CPANPLUS/t/04_CPANPLUS-Module.t CPANPLUS tests
+lib/CPANPLUS/t/05_CPANPLUS-Internals-Fetch.t CPANPLUS tests
+lib/CPANPLUS/t/06_CPANPLUS-Internals-Constants.t CPANPLUS tests
+lib/CPANPLUS/t/07_CPANPLUS-Internals-Extract.t CPANPLUS tests
+lib/CPANPLUS/t/08_CPANPLUS-Backend.t CPANPLUS tests
+lib/CPANPLUS/t/09_CPANPLUS-Internals-Search.t CPANPLUS tests
+lib/CPANPLUS/t/10_CPANPLUS-Error.t CPANPLUS tests
+lib/CPANPLUS/t/19_CPANPLUS-Dist.t CPANPLUS tests
+lib/CPANPLUS/t/20_CPANPLUS-Dist-MM.t CPANPLUS tests
+lib/CPANPLUS/t/21_CPANPLUS-Dist-No-Build.t CPANPLUS tests
+lib/CPANPLUS/t/30_CPANPLUS-Internals-Selfupdate.t CPANPLUS tests
+lib/CPANPLUS/t/40_CPANPLUS-Internals-Report.t CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.readme CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.readme CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.readme CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.readme CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-cpanplus/01mailrc.txt.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-cpanplus/02packages.details.txt.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-cpanplus/03modlist.data.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-cpanplus/sourcefiles.2.15.stored.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/CHECKSUMS CPANPLUS tests
+lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed CPANPLUS tests
+lib/CPANPLUS/t/inc/conf.pl CPANPLUS tests
lib/CPAN.pm Interface to Comprehensive Perl Archive Network
lib/CPAN/Queue.pm queueing system for CPAN.pm
lib/CPAN/SIGNATURE CPAN public key
@@ -2398,8 +2490,8 @@ lib/Pod/Simple/t/html02.t Pod::Simple test file
lib/Pod/Simple/t/html03.t Pod::Simple test file
lib/Pod/Simple/t/htmlbat.t Pod::Simple test file
lib/Pod/Simple/TiedOutFH.pm Pod::Simple::TiedOutFH
-lib/Pod/Simple/t/items.t Pod::Simple test file
lib/Pod/Simple/t/items02.t Pod::Simple test file
+lib/Pod/Simple/t/items.t Pod::Simple test file
lib/Pod/Simple/t/itemstar.t Pod::Simple test file
lib/Pod/Simple/t/junk1o.txt Pod::Simple test file
lib/Pod/Simple/t/junk1.pod Pod::Simple test file
@@ -3784,7 +3876,10 @@ util.h Dummy header
utils/c2ph.PL program to translate dbx stabs to perl
utils/config_data.PL Module::Build tool
utils/corelist.PL Module::CoreList
+utils/cpan2dist.PL the cpan2dist utility
utils/cpan.PL easily interact with CPAN from the command line
+utils/cpanp.PL the cpanp utility
+utils/cpanp-run-perl.PL the cpanp-run-perl utility
utils/dprofpp.PL Perl code profile post-processor
utils/enc2xs.PL Encode module generator
utils/h2ph.PL A thing to turn C .h files into perl .ph files
diff --git a/installperl b/installperl
index 0123e53666..f4742ee35a 100755
--- a/installperl
+++ b/installperl
@@ -830,7 +830,7 @@ sub installlib {
# the corelist script from lib/Module/CoreList/bin and ptar* in
# lib/Archive/Tar/bin, the config_data script in lib/Module/Build/scripts
# (they're installed later with other utils)
- return if $name =~ /^(?:cpan|instmodsh|prove|corelist|ptar|ptardiff|config_data)\z/;
+ return if $name =~ /^(?:cpan|instmodsh|prove|corelist|ptar|cpan2dist|cpanp|cpanp-run-perl|ptardiff|config_data)\z/;
# ignore the Makefiles
return if $name =~ /^makefile$/i;
# ignore the test extensions
diff --git a/lib/CPANPLUS.pm b/lib/CPANPLUS.pm
new file mode 100644
index 0000000000..b30aa7f0ee
--- /dev/null
+++ b/lib/CPANPLUS.pm
@@ -0,0 +1,271 @@
+package CPANPLUS;
+
+use strict;
+use Carp;
+
+use CPANPLUS::Error;
+use CPANPLUS::Backend;
+
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+BEGIN {
+ use Exporter ();
+ use vars qw( @EXPORT @ISA $VERSION );
+ @EXPORT = qw( shell fetch get install );
+ @ISA = qw( Exporter );
+ $VERSION = "0.78"; #have to hardcode or cpan.org gets unhappy
+}
+
+### purely for backward compatibility, so we can call it from the commandline:
+### perl -MCPANPLUS -e 'install Net::SMTP'
+sub install {
+ my $cpan = CPANPLUS::Backend->new;
+ my $mod = shift or (
+ error(loc("No module specified!")), return
+ );
+
+ if ( ref $mod ) {
+ error( loc( "You passed an object. Use %1 for OO style interaction",
+ 'CPANPLUS::Backend' ));
+ return;
+
+ } else {
+ my $obj = $cpan->module_tree($mod) or (
+ error(loc("No such module '%1'", $mod)),
+ return
+ );
+
+ my $ok = $obj->install;
+
+ $ok
+ ? msg(loc("Installing of %1 successful", $mod),1)
+ : msg(loc("Installing of %1 failed", $mod),1);
+
+ return $ok;
+ }
+}
+
+### simply downloads a module and stores it
+sub fetch {
+ my $cpan = CPANPLUS::Backend->new;
+
+ my $mod = shift or (
+ error(loc("No module specified!")), return
+ );
+
+ if ( ref $mod ) {
+ error( loc( "You passed an object. Use %1 for OO style interaction",
+ 'CPANPLUS::Backend' ));
+ return;
+
+ } else {
+ my $obj = $cpan->module_tree($mod) or (
+ error(loc("No such module '%1'", $mod)),
+ return
+ );
+
+ my $ok = $obj->fetch( fetchdir => '.' );
+
+ $ok
+ ? msg(loc("Fetching of %1 successful", $mod),1)
+ : msg(loc("Fetching of %1 failed", $mod),1);
+
+ return $ok;
+ }
+}
+
+### alias to fetch() due to compatibility with cpan.pm ###
+sub get { fetch(@_) }
+
+
+### purely for backwards compatibility, so we can call it from the commandline:
+### perl -MCPANPLUS -e 'shell'
+sub shell {
+ my $option = shift;
+
+ ### since the user can specify the type of shell they wish to start
+ ### when they call the shell() function, we have to eval the usage
+ ### of CPANPLUS::Shell so we can set up all the checks properly
+ eval { require CPANPLUS::Shell; CPANPLUS::Shell->import($option) };
+ die $@ if $@;
+
+ my $cpan = CPANPLUS::Shell->new();
+
+ $cpan->shell();
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+CPANPLUS - API & CLI access to the CPAN mirrors
+
+=head1 SYNOPSIS
+
+ ### standard invocation from the command line
+ $ cpanp
+ $ cpanp -i Some::Module
+
+ $ perl -MCPANPLUS -eshell
+ $ perl -MCPANPLUS -e'fetch Some::Module'
+
+
+=head1 DESCRIPTION
+
+The C<CPANPLUS> library is an API to the C<CPAN> mirrors and a
+collection of interactive shells, commandline programs, etc,
+that use this API.
+
+=head1 GUIDE TO DOCUMENTATION
+
+=head2 GENERAL USAGE
+
+This is the document you are currently reading. It describes
+basic usage and background information. Its main purpose is to
+assist the user who wants to learn how to invoke CPANPLUS
+and install modules from the commandline and to point you
+to more indepth reading if required.
+
+=head2 API REFERENCE
+
+The C<CPANPLUS> API is meant to let you programmatically
+interact with the C<CPAN> mirrors. The documentation in
+L<CPANPLUS::Backend> shows you how to create an object
+capable of interacting with those mirrors, letting you
+create & retrieve module objects.
+L<CPANPLUS::Module> shows you how you can use these module
+objects to perform actions like installing and testing.
+
+The default shell, documented in L<CPANPLUS::Shell::Default>
+is also scriptable. You can use its API to dispatch calls
+from your script to the CPANPLUS Shell.
+
+=cut
+
+=head1 COMMANDLINE TOOLS
+
+=head2 STARTING AN INTERACTIVE SHELL
+
+You can start an interactive shell by running either of
+the two following commands:
+
+ $ cpanp
+
+ $ perl -MCPANPLUS -eshell
+
+All commans available are listed in the interactive shells
+help menu. See C<cpanp -h> or L<CPANPLUS::Shell::Default>
+for instructions on using the default shell.
+
+=head2 CHOOSE A SHELL
+
+By running C<cpanp> without arguments, you will start up
+the shell specified in your config, which defaults to
+L<CPANPLUS::Shell::Default>. There are more shells available.
+C<CPANPLUS> itself ships with an emulation shell called
+L<CPANPLUS::Shell::Classic> that looks and feels just like
+the old C<CPAN.pm> shell.
+
+You can start this shell by typing:
+
+ $ perl -MCPANPLUS -e'shell Classic'
+
+Even more shells may be available from C<CPAN>.
+
+Note that if you have changed your default shell in your
+configuration, that shell will be used instead. If for
+some reason there was an error with your specified shell,
+you will be given the default shell.
+
+=head2 BUILDING PACKAGES
+
+C<cpan2dist> is a commandline tool to convert any distribution
+from C<CPAN> into a package in the format of your choice, like
+for example C<.deb> or C<FreeBSD ports>.
+
+See C<cpan2dist -h> for details.
+
+
+=head1 FUNCTIONS
+
+For quick access to common commands, you may use this module,
+C<CPANPLUS> rather than the full programmatic API situated in
+C<CPANPLUS::Backend>. This module offers the following functions:
+
+=head2 $bool = install( Module::Name | /A/AU/AUTHOR/Module-Name-1.tgz )
+
+This function requires the full name of the module, which is case
+sensitive. The module name can also be provided as a fully
+qualified file name, beginning with a I</>, relative to
+the /authors/id directory on a CPAN mirror.
+
+It will download, extract and install the module.
+
+=head2 $where = fetch( Module::Name | /A/AU/AUTHOR/Module-Name-1.tgz )
+
+Like install, fetch needs the full name of a module or the fully
+qualified file name, and is case sensitive.
+
+It will download the specified module to the current directory.
+
+=head2 $where = get( Module::Name | /A/AU/AUTHOR/Module-Name-1.tgz )
+
+Get is provided as an alias for fetch for compatibility with
+CPAN.pm.
+
+=head2 shell()
+
+Shell starts the default CPAN shell. You can also start the shell
+by using the C<cpanp> command, which will be installed in your
+perl bin.
+
+=head1 FAQ
+
+For frequently asked questions and answers, please consult the
+C<CPANPLUS::FAQ> manual.
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Shell::Default>, L<CPANPLUS::FAQ>, L<CPANPLUS::Backend>, L<CPANPLUS::Module>, L<cpanp>, L<cpan2dist>
+
+=head1 CONTACT INFORMATION
+
+=over 4
+
+=item * Bug reporting:
+I<bug-cpanplus@rt.cpan.org>
+
+=item * Questions & suggestions:
+I<cpanplus-devel@lists.sourceforge.net>
+
+=back
+
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Backend.pm b/lib/CPANPLUS/Backend.pm
new file mode 100644
index 0000000000..50b13c4628
--- /dev/null
+++ b/lib/CPANPLUS/Backend.pm
@@ -0,0 +1,1061 @@
+package CPANPLUS::Backend;
+
+use strict;
+
+
+use CPANPLUS::Error;
+use CPANPLUS::Configure;
+use CPANPLUS::Internals;
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Module;
+use CPANPLUS::Module::Author;
+use CPANPLUS::Backend::RV;
+
+use FileHandle;
+use File::Spec ();
+use File::Spec::Unix ();
+use Params::Check qw[check];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+$Params::Check::VERBOSE = 1;
+
+use vars qw[@ISA $VERSION];
+
+@ISA = qw[CPANPLUS::Internals];
+$VERSION = $CPANPLUS::Internals::VERSION;
+
+### mark that we're running under CPANPLUS to spawned processes
+$ENV{'PERL5_CPANPLUS_IS_RUNNING'} = $$;
+
+### XXX version.pm MAY format this version, if it's in use... :(
+### so for consistency, just call ->VERSION ourselves as well.
+$ENV{'PERL5_CPANPLUS_IS_VERSION'} = __PACKAGE__->VERSION;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Backend
+
+=head1 SYNOPSIS
+
+ my $cb = CPANPLUS::Backend->new( );
+ my $conf = $cb->configure_object;
+
+ my $author = $cb->author_tree('KANE');
+ my $mod = $cb->module_tree('Some::Module');
+ my $mod = $cb->parse_module( module => 'Some::Module' );
+
+ my @objs = $cb->search( type => TYPE,
+ allow => [...] );
+
+ $cb->flush('all');
+ $cb->reload_indices;
+ $cb->local_mirror;
+
+
+=head1 DESCRIPTION
+
+This module provides the programmer's interface to the C<CPANPLUS>
+libraries.
+
+=head1 ENVIRONMENT
+
+When C<CPANPLUS::Backend> is loaded, which is necessary for just
+about every <CPANPLUS> operation, the environment variable
+C<PERL5_CPANPLUS_IS_RUNNING> is set to the current process id.
+
+Additionally, the environment variable C<PERL5_CPANPLUS_IS_VERSION>
+will be set to the version of C<CPANPLUS::Backend>.
+
+This information might be useful somehow to spawned processes.
+
+=head1 METHODS
+
+=head2 $cb = CPANPLUS::Backend->new( [CONFIGURE_OBJ] )
+
+This method returns a new C<CPANPLUS::Backend> object.
+This also initialises the config corresponding to this object.
+You have two choices in this:
+
+=over 4
+
+=item Provide a valid C<CPANPLUS::Configure> object
+
+This will be used verbatim.
+
+=item No arguments
+
+Your default config will be loaded and used.
+
+=back
+
+New will return a C<CPANPLUS::Backend> object on success and die on
+failure.
+
+=cut
+
+sub new {
+ my $class = shift;
+ my $conf;
+
+ if( $_[0] && IS_CONFOBJ->( conf => $_[0] ) ) {
+ $conf = shift;
+ } else {
+ $conf = CPANPLUS::Configure->new() or return;
+ }
+
+ my $self = $class->SUPER::_init( _conf => $conf );
+
+ return $self;
+}
+
+=pod
+
+=head2 $href = $cb->module_tree( [@modules_names_list] )
+
+Returns a reference to the CPANPLUS module tree.
+
+If you give it any arguments, they will be treated as module names
+and C<module_tree> will try to look up these module names and
+return the corresponding module objects instead.
+
+See L<CPANPLUS::Module> for the operations you can perform on a
+module object.
+
+=cut
+
+sub module_tree {
+ my $self = shift;
+ my $modtree = $self->_module_tree;
+
+ if( @_ ) {
+ my @rv;
+ for my $name ( grep { defined } @_) {
+ push @rv, $modtree->{$name} || '';
+ }
+ return @rv == 1 ? $rv[0] : @rv;
+ } else {
+ return $modtree;
+ }
+}
+
+=pod
+
+=head2 $href = $cb->author_tree( [@author_names_list] )
+
+Returns a reference to the CPANPLUS author tree.
+
+If you give it any arguments, they will be treated as author names
+and C<author_tree> will try to look up these author names and
+return the corresponding author objects instead.
+
+See L<CPANPLUS::Module::Author> for the operations you can perform on
+an author object.
+
+=cut
+
+sub author_tree {
+ my $self = shift;
+ my $authtree = $self->_author_tree;
+
+ if( @_ ) {
+ my @rv;
+ for my $name (@_) {
+ push @rv, $authtree->{$name} || '';
+ }
+ return @rv == 1 ? $rv[0] : @rv;
+ } else {
+ return $authtree;
+ }
+}
+
+=pod
+
+=head2 $conf = $cb->configure_object ()
+
+Returns a copy of the C<CPANPLUS::Configure> object.
+
+See L<CPANPLUS::Configure> for operations you can perform on a
+configure object.
+
+=cut
+
+sub configure_object { return shift->_conf() };
+
+=head2 $su = $cb->selfupdate_object;
+
+Returns a copy of the C<CPANPLUS::Selfupdate> object.
+
+See the L<CPANPLUS::Selfupdate> manpage for the operations
+you can perform on the selfupdate object.
+
+=cut
+
+sub selfupdate_object { return shift->_selfupdate() };
+
+=pod
+
+=head2 @mods = $cb->search( type => TYPE, allow => AREF, [data => AREF, verbose => BOOL] )
+
+C<search> enables you to search for either module or author objects,
+based on their data. The C<type> you can specify is any of the
+accessors specified in C<CPANPLUS::Module::Author> or
+C<CPANPLUS::Module>. C<search> will determine by the C<type> you
+specified whether to search by author object or module object.
+
+You have to specify an array reference of regular expressions or
+strings to match against. The rules used for this array ref are the
+same as in C<Params::Check>, so read that manpage for details.
+
+The search is an C<or> search, meaning that if C<any> of the criteria
+match, the search is considered to be successful.
+
+You can specify the result of a previous search as C<data> to limit
+the new search to these module or author objects, rather than the
+entire module or author tree. This is how you do C<and> searches.
+
+Returns a list of module or author objects on success and false
+on failure.
+
+See L<CPANPLUS::Module> for the operations you can perform on a
+module object.
+See L<CPANPLUS::Module::Author> for the operations you can perform on
+an author object.
+
+=cut
+
+sub search {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my ($data,$type);
+ my $tmpl = {
+ type => { required => 1, allow => [CPANPLUS::Module->accessors(),
+ CPANPLUS::Module::Author->accessors()], store => \$type },
+ allow => { required => 1, default => [ ], strict_type => 1 },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### figure out whether it was an author or a module search
+ ### when ambiguous, it'll be an author search.
+ my $aref;
+ if( grep { $type eq $_ } CPANPLUS::Module::Author->accessors() ) {
+ $aref = $self->_search_author_tree( %$args );
+ } else {
+ $aref = $self->_search_module_tree( %$args );
+ }
+
+ return @$aref if $aref;
+ return;
+}
+
+=pod
+
+=head2 $backend_rv = $cb->fetch( modules => \@mods )
+
+Fetches a list of modules. C<@mods> can be a list of distribution
+names, module names or module objects--basically anything that
+L<parse_module> can understand.
+
+See the equivalent method in C<CPANPLUS::Module> for details on
+other options you can pass.
+
+Since this is a multi-module method call, the return value is
+implemented as a C<CPANPLUS::Backend::RV> object. Please consult
+that module's documentation on how to interpret the return value.
+
+=head2 $backend_rv = $cb->extract( modules => \@mods )
+
+Extracts a list of modules. C<@mods> can be a list of distribution
+names, module names or module objects--basically anything that
+L<parse_module> can understand.
+
+See the equivalent method in C<CPANPLUS::Module> for details on
+other options you can pass.
+
+Since this is a multi-module method call, the return value is
+implemented as a C<CPANPLUS::Backend::RV> object. Please consult
+that module's documentation on how to interpret the return value.
+
+=head2 $backend_rv = $cb->install( modules => \@mods )
+
+Installs a list of modules. C<@mods> can be a list of distribution
+names, module names or module objects--basically anything that
+L<parse_module> can understand.
+
+See the equivalent method in C<CPANPLUS::Module> for details on
+other options you can pass.
+
+Since this is a multi-module method call, the return value is
+implemented as a C<CPANPLUS::Backend::RV> object. Please consult
+that module's documentation on how to interpret the return value.
+
+=head2 $backend_rv = $cb->readme( modules => \@mods )
+
+Fetches the readme for a list of modules. C<@mods> can be a list of
+distribution names, module names or module objects--basically
+anything that L<parse_module> can understand.
+
+See the equivalent method in C<CPANPLUS::Module> for details on
+other options you can pass.
+
+Since this is a multi-module method call, the return value is
+implemented as a C<CPANPLUS::Backend::RV> object. Please consult
+that module's documentation on how to interpret the return value.
+
+=head2 $backend_rv = $cb->files( modules => \@mods )
+
+Returns a list of files used by these modules if they are installed.
+C<@mods> can be a list of distribution names, module names or module
+objects--basically anything that L<parse_module> can understand.
+
+See the equivalent method in C<CPANPLUS::Module> for details on
+other options you can pass.
+
+Since this is a multi-module method call, the return value is
+implemented as a C<CPANPLUS::Backend::RV> object. Please consult
+that module's documentation on how to interpret the return value.
+
+=head2 $backend_rv = $cb->distributions( modules => \@mods )
+
+Returns a list of module objects representing all releases for this
+module on success.
+C<@mods> can be a list of distribution names, module names or module
+objects, basically anything that L<parse_module> can understand.
+
+See the equivalent method in C<CPANPLUS::Module> for details on
+other options you can pass.
+
+Since this is a multi-module method call, the return value is
+implemented as a C<CPANPLUS::Backend::RV> object. Please consult
+that module's documentation on how to interpret the return value.
+
+=cut
+
+### XXX add direcotry_tree, packlist etc? or maybe remove files? ###
+for my $func (qw[fetch extract install readme files distributions]) {
+ no strict 'refs';
+
+ *$func = sub {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ local $Params::Check::NO_DUPLICATES = 1;
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my ($mods);
+ my $tmpl = {
+ modules => { default => [], strict_type => 1,
+ required => 1, store => \$mods },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### make them all into module objects ###
+ my %mods = map {$_ => $self->parse_module(module => $_) || ''} @$mods;
+
+ my $flag; my $href;
+ while( my($name,$obj) = each %mods ) {
+ $href->{$name} = IS_MODOBJ->( mod => $obj )
+ ? $obj->$func( %$args )
+ : undef;
+
+ $flag++ unless $href->{$name};
+ }
+
+ return CPANPLUS::Backend::RV->new(
+ function => $func,
+ ok => !$flag,
+ rv => $href,
+ args => \%hash,
+ );
+ }
+}
+
+=pod
+
+=head2 $mod_obj = $cb->parse_module( module => $modname|$distname|$modobj )
+
+C<parse_module> tries to find a C<CPANPLUS::Module> object that
+matches your query. Here's a list of examples you could give to
+C<parse_module>;
+
+=over 4
+
+=item Text::Bastardize
+
+=item Text-Bastardize
+
+=item Text-Bastardize-1.06
+
+=item AYRNIEU/Text-Bastardize
+
+=item AYRNIEU/Text-Bastardize-1.06
+
+=item AYRNIEU/Text-Bastardize-1.06.tar.gz
+
+=item http://example.com/Text-Bastardize-1.06.tar.gz
+
+=item file:///tmp/Text-Bastardize-1.06.tar.gz
+
+=back
+
+These items would all come up with a C<CPANPLUS::Module> object for
+C<Text::Bastardize>. The ones marked explicitly as being version 1.06
+would give back a C<CPANPLUS::Module> object of that version.
+Even if the version on CPAN is currently higher.
+
+If C<parse_module> is unable to actually find the module you are looking
+for in its module tree, but you supplied it with an author, module
+and version part in a distribution name or URI, it will create a fake
+C<CPANPLUS::Module> object for you, that you can use just like the
+real thing.
+
+See L<CPANPLUS::Module> for the operations you can perform on a
+module object.
+
+If even this fancy guessing doesn't enable C<parse_module> to create
+a fake module object for you to use, it will warn about an error and
+return false.
+
+=cut
+
+sub parse_module {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my $mod;
+ my $tmpl = {
+ module => { required => 1, store => \$mod },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ return $mod if IS_MODOBJ->( module => $mod );
+
+ ### ok, so it's not a module object, but a ref nonetheless?
+ ### what are you smoking?
+ if( ref $mod ) {
+ error(loc("Can not parse module string from reference '%1'", $mod ));
+ return;
+ }
+
+ ### check only for allowed characters in a module name
+ unless( $mod =~ /[^\w:]/ ) {
+
+ ### perhaps we can find it in the module tree?
+ my $maybe = $self->module_tree($mod);
+ return $maybe if IS_MODOBJ->( module => $maybe );
+ }
+
+ ### ok, so it looks like a distribution then?
+ my @parts = split '/', $mod;
+ my $dist = pop @parts;
+
+ ### ah, it's a URL
+ if( $mod =~ m|\w+://.+| ) {
+ my $modobj = CPANPLUS::Module::Fake->new(
+ module => $dist,
+ version => 0,
+ package => $dist,
+ path => File::Spec::Unix->catdir(
+ $conf->_get_mirror('base'),
+ UNKNOWN_DL_LOCATION ),
+ author => CPANPLUS::Module::Author::Fake->new
+ );
+
+ ### set the fetch_from accessor so we know to by pass the
+ ### usual mirrors
+ $modobj->status->_fetch_from( $mod );
+
+ return $modobj;
+ }
+
+ ### perhaps we can find it's a third party module?
+ { my $modobj = CPANPLUS::Module::Fake->new(
+ module => $mod,
+ version => 0,
+ package => $dist,
+ path => File::Spec::Unix->catdir(
+ $conf->_get_mirror('base'),
+ UNKNOWN_DL_LOCATION ),
+ author => CPANPLUS::Module::Author::Fake->new
+ );
+ if( $modobj->is_third_party ) {
+ my $info = $modobj->third_party_information;
+
+ $modobj->author->author( $info->{author} );
+ $modobj->author->email( $info->{author_url} );
+ $modobj->description( $info->{url} );
+
+ return $modobj;
+ }
+ }
+
+ unless( $dist ) {
+ error( loc("%1 is not a proper distribution name!", $mod) );
+ return;
+ }
+
+ ### there's wonky uris out there, like this:
+ ### E/EY/EYCK/Net/Lite/Net-Lite-FTP-0.091
+ ### compensate for that
+ my $author;
+ ### you probably have an A/AB/ABC/....../Dist.tgz type uri
+ if( (defined $parts[0] and length $parts[0] == 1) and
+ (defined $parts[1] and length $parts[1] == 2) and
+ $parts[2] =~ /^$parts[0]/i and $parts[2] =~ /^$parts[1]/i
+ ) {
+ splice @parts, 0, 2; # remove the first 2 entries from the list
+ $author = shift @parts; # this is the actual author name then
+
+ ### we''ll assume a ABC/..../Dist.tgz
+ } else {
+ $author = shift @parts || '';
+ }
+
+ my($pkg, $version, $ext) =
+ $self->_split_package_string( package => $dist );
+
+ ### translate a distribution into a module name ###
+ my $guess = $pkg;
+ $guess =~ s/-/::/g if $guess;
+
+ my $maybe = $self->module_tree( $guess );
+ if( IS_MODOBJ->( module => $maybe ) ) {
+
+ ### maybe you asked for a package instead
+ if ( $maybe->package eq $mod ) {
+ return $maybe;
+
+ ### perhaps an outdated version instead?
+ } elsif ( $version ) {
+ my $auth_obj; my $path;
+
+ ### did you give us an author part? ###
+ if( $author ) {
+ $auth_obj = CPANPLUS::Module::Author::Fake->new(
+ _id => $maybe->_id,
+ cpanid => uc $author,
+ author => uc $author,
+ );
+ $path = File::Spec::Unix->catdir(
+ $conf->_get_mirror('base'),
+ substr(uc $author, 0, 1),
+ substr(uc $author, 0, 2),
+ uc $author,
+ @parts, #possible sub dirs
+ );
+ } else {
+ $auth_obj = $maybe->author;
+ $path = $maybe->path;
+ }
+
+ if( $maybe->package_name eq $pkg ) {
+
+ my $modobj = CPANPLUS::Module::Fake->new(
+ module => $maybe->module,
+ version => $version,
+ package => $pkg . '-' . $version . '.' .
+ $maybe->package_extension,
+ path => $path,
+ author => $auth_obj,
+ _id => $maybe->_id
+ );
+ return $modobj;
+
+ ### you asked for a specific version?
+ ### assume our $maybe is the one you wanted,
+ ### and fix up the version..
+ } else {
+
+ my $modobj = $maybe->clone;
+ $modobj->version( $version );
+ $modobj->package(
+ $maybe->package_name .'-'.
+ $version .'.'.
+ $maybe->package_extension
+ );
+
+ ### you wanted a specific author, but it's not the one
+ ### from the module tree? we'll fix it up
+ if( $author and $author ne $modobj->author->cpanid ) {
+ $modobj->author( $auth_obj );
+ $modobj->path( $path );
+ }
+
+ return $modobj;
+ }
+
+ ### you didn't care about a version, so just return the object then
+ } elsif ( !$version ) {
+ return $maybe;
+ }
+
+ ### ok, so we can't find it, and it's not an outdated dist either
+ ### perhaps we can fake one based on the author name and so on
+ } elsif ( $author and $version ) {
+
+ ### be extra friendly and pad the .tar.gz suffix where needed
+ ### it's just a guess of course, but most dists are .tar.gz
+ $dist .= '.tar.gz' unless $dist =~ /\.[A-Za-z]+$/;
+
+ ### XXX duplication from above for generating author obj + path...
+ my $modobj = CPANPLUS::Module::Fake->new(
+ module => $guess,
+ version => $version,
+ package => $dist,
+ author => CPANPLUS::Module::Author::Fake->new(
+ author => uc $author,
+ cpanid => uc $author,
+ _id => $self->_id,
+ ),
+ path => File::Spec::Unix->catdir(
+ $conf->_get_mirror('base'),
+ substr(uc $author, 0, 1),
+ substr(uc $author, 0, 2),
+ uc $author,
+ @parts, #possible subdirs
+ ),
+ _id => $self->_id,
+ );
+
+ return $modobj;
+
+ ### face it, we have /no/ idea what he or she wants...
+ ### let's start putting the blame somewhere
+ } else {
+
+ unless( $author ) {
+ error( loc( "'%1' does not contain an author part", $mod ) );
+ }
+
+ error( loc( "Cannot find '%1' in the module tree", $mod ) );
+ }
+
+ return;
+}
+
+=pod
+
+=head2 $bool = $cb->reload_indices( [update_source => BOOL, verbose => BOOL] );
+
+This method reloads the source files.
+
+If C<update_source> is set to true, this will fetch new source files
+from your CPAN mirror. Otherwise, C<reload_indices> will do its
+usual cache checking and only update them if they are out of date.
+
+By default, C<update_source> will be false.
+
+The verbose setting defaults to what you have specified in your
+config file.
+
+Returns true on success and false on failure.
+
+=cut
+
+sub reload_indices {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+ my $tmpl = {
+ update_source => { default => 0, allow => [qr/^\d$/] },
+ verbose => { default => $conf->get_conf('verbose') },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### make a call to the internal _module_tree, so it triggers cache
+ ### file age
+ my $uptodate = $self->_check_trees( %$args );
+
+
+ return 1 if $self->_build_trees(
+ uptodate => $uptodate,
+ use_stored => 0,
+ verbose => $conf->get_conf('verbose'),
+ );
+
+ error( loc( "Error rebuilding source trees!" ) );
+
+ return;
+}
+
+=pod
+
+=head2 $bool = $cb->flush(CACHE_NAME)
+
+This method allows flushing of caches.
+There are several things which can be flushed:
+
+=over 4
+
+=item * C<methods>
+
+The return status of methods which have been attempted, such as
+different ways of fetching files. It is recommended that automatic
+flushing be used instead.
+
+=item * C<hosts>
+
+The return status of URIs which have been attempted, such as
+different hosts of fetching files. It is recommended that automatic
+flushing be used instead.
+
+=item * C<modules>
+
+Information about modules such as prerequisites and whether
+installation succeeded, failed, or was not attempted.
+
+=item * C<lib>
+
+This resets PERL5LIB, which is changed to ensure that while installing
+modules they are in our @INC.
+
+=item * C<load>
+
+This resets the cache of modules we've attempted to load, but failed.
+This enables you to load them again after a failed load, if they
+somehow have become available.
+
+=item * C<all>
+
+Flush all of the aforementioned caches.
+
+=back
+
+Returns true on success and false on failure.
+
+=cut
+
+sub flush {
+ my $self = shift;
+ my $type = shift or return;
+
+ my $cache = {
+ methods => [ qw( methods load ) ],
+ hosts => [ qw( hosts ) ],
+ modules => [ qw( modules lib) ],
+ lib => [ qw( lib ) ],
+ load => [ qw( load ) ],
+ all => [ qw( hosts lib modules methods load ) ],
+ };
+
+ my $aref = $cache->{$type}
+ or (
+ error( loc("No such cache '%1'", $type) ),
+ return
+ );
+
+ return $self->_flush( list => $aref );
+}
+
+=pod
+
+=head2 @mods = $cb->installed()
+
+Returns a list of module objects of all your installed modules.
+If an error occurs, it will return false.
+
+See L<CPANPLUS::Module> for the operations you can perform on a
+module object.
+
+=cut
+
+sub installed {
+ my $self = shift;
+ my $aref = $self->_all_installed;
+
+ return @$aref if $aref;
+ return;
+}
+
+=pod
+
+=head2 $bool = $cb->local_mirror([path => '/dir/to/save/to', index_files => BOOL, force => BOOL, verbose => BOOL] )
+
+Creates a local mirror of CPAN, of only the most recent sources in a
+location you specify. If you set this location equal to a custom host
+in your C<CPANPLUS::Config> you can use your local mirror to install
+from.
+
+It takes the following arguments:
+
+=over 4
+
+=item path
+
+The location where to create the local mirror.
+
+=item index_files
+
+Enable/disable fetching of index files. This is ok if you don't plan
+to use the local mirror as your primary sites, or if you'd like
+up-to-date index files be fetched from elsewhere.
+
+Defaults to true.
+
+=item force
+
+Forces refetching of packages, even if they are there already.
+
+Defaults to whatever setting you have in your C<CPANPLUS::Config>.
+
+=item verbose
+
+Prints more messages about what its doing.
+
+Defaults to whatever setting you have in your C<CPANPLUS::Config>.
+
+=back
+
+Returns true on success and false on error.
+
+=cut
+
+sub local_mirror {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my($path, $index, $force, $verbose);
+ my $tmpl = {
+ path => { default => $conf->get_conf('base'),
+ store => \$path },
+ index_files => { default => 1, store => \$index },
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ unless( -d $path ) {
+ $self->_mkdir( dir => $path )
+ or( error( loc( "Could not create '%1', giving up", $path ) ),
+ return
+ );
+ } elsif ( ! -w _ ) {
+ error( loc( "Could not write to '%1', giving up", $path ) );
+ return;
+ }
+
+ my $flag;
+ AUTHOR: {
+ for my $auth ( sort { $a->cpanid cmp $b->cpanid }
+ values %{$self->author_tree}
+ ) {
+
+ MODULE: {
+ my $i;
+ for my $mod ( $auth->modules ) {
+ my $fetchdir = File::Spec->catdir( $path, $mod->path );
+
+ my %opts = (
+ verbose => $verbose,
+ force => $force,
+ fetchdir => $fetchdir,
+ );
+
+ ### only do this the for the first module ###
+ unless( $i++ ) {
+ $mod->_get_checksums_file(
+ %opts
+ ) or (
+ error( loc( "Could not fetch %1 file, " .
+ "skipping author '%2'",
+ CHECKSUMS, $auth->cpanid ) ),
+ $flag++, next AUTHOR
+ );
+ }
+
+ $mod->fetch( %opts )
+ or( error( loc( "Could not fetch '%1'", $mod->module ) ),
+ $flag++, next MODULE
+ );
+ } }
+ } }
+
+ if( $index ) {
+ for my $name (qw[auth dslip mod]) {
+ $self->_update_source(
+ name => $name,
+ verbose => $verbose,
+ path => $path,
+ ) or ( $flag++, next );
+ }
+ }
+
+ return !$flag;
+}
+
+=pod
+
+=head2 $file = $cb->autobundle([path => OUTPUT_PATH, force => BOOL, verbose => BOOL])
+
+Writes out a snapshot of your current installation in C<CPAN> bundle
+style. This can then be used to install the same modules for a
+different or on a different machine.
+
+It will, by default, write to an 'autobundle' directory under your
+cpanplus homedirectory, but you can override that by supplying a
+C<path> argument.
+
+It will return the location of the output file on success and false on
+failure.
+
+=cut
+
+sub autobundle {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my($path,$force,$verbose);
+ my $tmpl = {
+ force => { default => $conf->get_conf('force'), store => \$force },
+ verbose => { default => $conf->get_conf('verbose'), store => \$verbose },
+ path => { default => File::Spec->catdir(
+ $conf->get_conf('base'),
+ $self->_perl_version( perl => $^X ),
+ $conf->_get_build('distdir'),
+ $conf->_get_build('autobundle') ),
+ store => \$path },
+ };
+
+ check($tmpl, \%hash) or return;
+
+ unless( -d $path ) {
+ $self->_mkdir( dir => $path )
+ or( error(loc("Could not create directory '%1'", $path ) ),
+ return
+ );
+ }
+
+ my $name; my $file;
+ { ### default filename for the bundle ###
+ my($year,$month,$day) = (localtime)[5,4,3];
+ $year += 1900; $month++;
+
+ my $ext = 0;
+
+ my $prefix = $conf->_get_build('autobundle_prefix');
+ my $format = "${prefix}_%04d_%02d_%02d_%02d";
+
+ BLOCK: {
+ $name = sprintf( $format, $year, $month, $day, $ext);
+
+ $file = File::Spec->catfile( $path, $name . '.pm' );
+
+ -f $file ? ++$ext && redo BLOCK : last BLOCK;
+ }
+ }
+ my $fh;
+ unless( $fh = FileHandle->new( ">$file" ) ) {
+ error( loc( "Could not open '%1' for writing: %2", $file, $! ) );
+ return;
+ }
+
+ my $string = join "\n\n",
+ map {
+ join ' ',
+ $_->module,
+ ($_->installed_version(verbose => 0) || 'undef')
+ } sort {
+ $a->module cmp $b->module
+ } $self->installed;
+
+ my $now = scalar localtime;
+ my $head = '=head1';
+ my $pkg = __PACKAGE__;
+ my $version = $self->VERSION;
+ my $perl_v = join '', `$^X -V`;
+
+ print $fh <<EOF;
+package $name
+
+\$VERSION = '0.01';
+
+1;
+
+__END__
+
+$head NAME
+
+$name - Snapshot of your installation at $now
+
+$head SYNOPSIS
+
+perl -MCPANPLUS -e "install $name"
+
+$head CONTENTS
+
+$string
+
+$head CONFIGURATION
+
+$perl_v
+
+$head AUTHOR
+
+This bundle has been generated autotomatically by
+ $pkg $version
+
+EOF
+
+ close $fh;
+
+ return $file;
+}
+
+1;
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Configure>, L<CPANPLUS::Module>, L<CPANPLUS::Module::Author>
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
+__END__
+
+todo:
+sub dist { # not sure about this one -- probably already done
+ enough in Module.pm
+sub reports { # in Module.pm, wrapper here
+
+
diff --git a/lib/CPANPLUS/Backend/RV.pm b/lib/CPANPLUS/Backend/RV.pm
new file mode 100644
index 0000000000..9edbe0452c
--- /dev/null
+++ b/lib/CPANPLUS/Backend/RV.pm
@@ -0,0 +1,144 @@
+package CPANPLUS::Backend::RV;
+
+use strict;
+use vars qw[$STRUCT];
+
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+use IPC::Cmd qw[can_run run];
+use Params::Check qw[check];
+
+use base 'Object::Accessor';
+
+local $Params::Check::VERBOSE = 1;
+
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Backend::RV
+
+=head1 SYNOPSIS
+
+ ### create a CPANPLUS::Backend::RV object
+ $backend_rv = CPANPLUS::Backend::RV->new(
+ ok => $boolean,
+ args => $args,
+ rv => $return_value
+ function => $calling_function );
+
+ ### if you have a CPANPLUS::Backend::RV object
+ $passed_args = $backend_rv->args; # args passed to function
+ $ok = $backend_rv->ok; # boolean indication overall
+ # result of the call
+ $function = $backend_rv->fucntion # name of the calling
+ # function
+ $rv = $backend_rv->rv # the actual return value
+ # of the calling function
+
+=head1 DESCRIPTION
+
+This module provides return value objects for multi-module
+calls to CPANPLUS::Backend. In boolean context, it returns the status
+of the overall result (ie, the same as the C<ok> method would).
+
+=head1 METHODS
+
+=head2 new( ok => BOOL, args => DATA, rv => DATA, [function => $method_name] )
+
+Creates a new CPANPLUS::Backend::RV object from the data provided.
+This method should only be called by CPANPLUS::Backend functions.
+The accessors may be used by users inspecting an RV object.
+
+All the argument names can be used as accessors later to retrieve the
+data.
+
+Arguments:
+
+=over 4
+
+=item ok
+
+Boolean indicating overall success
+
+=item args
+
+The arguments provided to the function that returned this rv object.
+Useful to inspect later to see what was actually passed to the function
+in case of an error.
+
+=item rv
+
+An arbitrary data structure that has the detailed return values of each
+of your multi-module calls.
+
+=item function
+
+The name of the function that created this rv object.
+Can be explicitly passed. If not, C<new()> will try to deduce the name
+from C<caller()> information.
+
+=back
+
+=cut
+
+sub new {
+ my $class = shift;
+ my %hash = @_;
+
+ my $tmpl = {
+ ok => { required => 1, allow => BOOLEANS },
+ args => { required => 1 },
+ rv => { required => 1 },
+ function => { default => CALLING_FUNCTION->() },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+ my $self = bless {}, $class;
+
+# $self->mk_accessors( qw[ok args function rv] );
+ $self->mk_accessors( keys %$tmpl );
+
+ ### set the values passed in the struct ###
+ while( my($key,$val) = each %$args ) {
+ $self->$key( $val );
+ }
+
+ return $self;
+}
+
+sub _ok { return shift->ok }
+#sub _stringify { Carp::carp( "stringifying!" ); overload::StrVal( shift ) }
+
+### make it easier to check if($rv) { foo() }
+### this allows people to not have to explicitly say
+### if( $rv->ok ) { foo() }
+### XXX add an explicit stringify, so it doesn't fall back to "bool"? :(
+use overload bool => \&_ok,
+# '""' => \&_stringify,
+ fallback => 1;
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
+1;
diff --git a/lib/CPANPLUS/Config.pm b/lib/CPANPLUS/Config.pm
new file mode 100644
index 0000000000..2516559f78
--- /dev/null
+++ b/lib/CPANPLUS/Config.pm
@@ -0,0 +1,264 @@
+package CPANPLUS::Config;
+
+use strict;
+use warnings;
+
+use base 'Object::Accessor';
+
+use base 'CPANPLUS::Internals::Utils';
+
+use Config;
+use File::Spec;
+use Module::Load;
+use CPANPLUS;
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+use File::Basename qw[dirname];
+use IPC::Cmd qw[can_run];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+use Module::Load::Conditional qw[check_install];
+
+my $Conf = {
+ '_fetch' => {
+ 'blacklist' => [ 'ftp' ],
+ },
+ 'conf' => {
+ ### default host list
+ 'hosts' => [
+ {
+ 'scheme' => 'ftp',
+ 'path' => '/pub/CPAN/',
+ 'host' => 'ftp.cpan.org'
+ },
+ {
+ 'scheme' => 'http',
+ 'path' => '/',
+ 'host' => 'www.cpan.org'
+ },
+ {
+ 'scheme' => 'ftp',
+ 'path' => '/pub/CPAN/',
+ 'host' => 'ftp.nl.uu.net'
+ },
+ {
+ 'scheme' => 'ftp',
+ 'path' => '/pub/CPAN/',
+ 'host' => 'cpan.valueclick.com'
+ },
+ {
+ 'scheme' => 'ftp',
+ 'path' => '/pub/languages/perl/CPAN/',
+ 'host' => 'ftp.funet.fi'
+ }
+ ],
+ 'allow_build_interactivity' => 1,
+ 'base' => File::Spec->catdir(
+ __PACKAGE__->_home_dir, DOT_CPANPLUS ),
+ 'buildflags' => '',
+ 'cpantest' => 0,
+ 'cpantest_mx' => '',
+ 'debug' => 0,
+ 'dist_type' => '',
+ 'email' => DEFAULT_EMAIL,
+ 'extractdir' => '',
+ 'fetchdir' => '',
+ 'flush' => 1,
+ 'force' => 0,
+ 'lib' => [],
+ 'makeflags' => '',
+ 'makemakerflags' => '',
+ 'md5' => (
+ check_install( module => 'Digest::MD5' ) ? 1 : 0 ),
+ 'no_update' => 0,
+ 'passive' => 1,
+ ### if we dont have c::zlib, we'll need to use /bin/tar or we
+ ### can not extract any files. Good time to change the default
+ 'prefer_bin' => (eval {require Compress::Zlib; 1}?0:1),
+ 'prefer_makefile' => 1,
+ 'prereqs' => PREREQ_ASK,
+ 'shell' => 'CPANPLUS::Shell::Default',
+ 'show_startup_tip' => 1,
+ 'signature' => ( (can_run( 'gpg' ) ||
+ check_install( module => 'Crypt::OpenPGP' ))?1:0 ),
+ 'skiptest' => 0,
+ 'storable' => (
+ check_install( module => 'Storable' ) ? 1 : 0 ),
+ 'timeout' => 300,
+ 'verbose' => $ENV{PERL5_CPANPLUS_VERBOSE} || 0,
+ 'write_install_logs' => 1,
+ },
+ ### Paths get stripped of whitespace on win32 in the constructor
+ ### sudo gets emptied if there's no need for it in the constructor
+ 'program' => {
+ 'editor' => ( $ENV{'EDITOR'} || $ENV{'VISUAL'} ||
+ can_run('vi') || can_run('pico')
+ ),
+ 'make' => ( can_run($Config{'make'}) || can_run('make') ),
+ 'pager' => ( $ENV{'PAGER'} || can_run('less') || can_run('more') ),
+ ### no one uses this feature anyway, and it's only working for EU::MM
+ ### and not for module::build
+ #'perl' => '',
+ 'shell' => ( $^O eq 'MSWin32' ? $ENV{COMSPEC} : $ENV{SHELL} ),
+ 'sudo' => ( $> # check for all install dirs!
+ # installsiteman3dir is a 5.8'ism.. don't check
+ # it on 5.6.x...
+ ? ( -w $Config{'installsitelib'} &&
+ ( defined $Config{'installsiteman3dir'} &&
+ -w $Config{'installsiteman3dir'}
+ ) &&
+ -w $Config{'installsitebin'}
+ ? undef
+ : can_run('sudo')
+ )
+ : can_run('sudo')
+ ),
+ ### perlwrapper that allows us to turn on autoflushing
+ 'perlwrapper' => ( ### parallel to your cpanp/cpanp-boxed
+ do { my $f = File::Spec->rel2abs(
+ File::Spec->catdir(
+ dirname($0), 'cpanp-run-perl'
+ )
+ );
+ -e $f ? $f : undef
+ } ||
+
+ ### parallel to your CPANPLUS.pm:
+ ### $INC{cpanplus}/../bin/cpanp-run-perl
+ do { my $f = File::Spec->rel2abs(
+ File::Spec->catdir(
+ dirname( $INC{'CPANPLUS.pm'} ),
+ '..', # lib dir
+ 'bin', # bin dir
+ 'cpanp-run-perl'
+ )
+ );
+ -e $f ? $f : undef
+ } ||
+ ### you installed CPANPLUS in a custom prefix,
+ ### so go paralel to /that/. PREFIX=/tmp/cp
+ ### would put cpanp-run-perl in /tmp/cp/bin and
+ ### CPANPLUS.pm in
+ ### /tmp/cp/lib/perl5/site_perl/5.8.8
+ do { my $f = File::Spec->rel2abs(
+ File::Spec->catdir(
+ dirname( $INC{'CPANPLUS.pm'} ),
+ '..', '..', '..', '..', # 4x updir
+ 'bin', # bin dir
+ 'cpanp-run-perl'
+ )
+ );
+ -e $f ? $f : undef
+ } ||
+
+ ### in your path -- take this one last, the
+ ### previous two assume extracted tarballs
+ ### or user installs
+ ### note that we don't use 'can_run' as it's
+ ### not an executable, just a wrapper...
+ do { my $rv;
+ for (split(/\Q$Config::Config{path_sep}\E/,
+ $ENV{PATH}), File::Spec->curdir
+ ) {
+ my $path = File::Spec->catfile(
+ $_, 'cpanp-run-perl' );
+ if( -e $path ) {
+ $rv = $path;
+ last;
+ }
+ }
+
+ $rv || undef;
+ } ||
+
+ ### XXX try to be a no-op instead then..
+ ### cross your fingers...
+ ### pass '-P' to perl: "run program through C
+ ### preprocessor before compilation"
+ do {
+ error(loc(
+ "Could not find the '%1' in your path".
+ "--this may be a problem.\n".
+ "Please locate this program and set ".
+ "your '%1' config entry to its path.\n".
+ "Attempting to provide a reasonable ".
+ "fallback...",
+ 'cpanp-run-perl', 'perlwrapper'
+ ));
+ '-P'
+ },
+ ),
+ },
+
+ ### _source, _build and _mirror are supposed to be static
+ ### no changes should be needed unless pause/cpan changes
+ '_source' => {
+ 'hosts' => 'MIRRORED.BY',
+ 'auth' => '01mailrc.txt.gz',
+ 'stored' => 'sourcefiles',
+ 'dslip' => '03modlist.data.gz',
+ 'update' => '86400',
+ 'mod' => '02packages.details.txt.gz'
+ },
+ '_build' => {
+ 'plugins' => 'plugins',
+ 'moddir' => 'build',
+ 'startdir' => '',
+ 'distdir' => 'dist',
+ 'autobundle' => 'autobundle',
+ 'autobundle_prefix' => 'Snapshot',
+ 'autdir' => 'authors',
+ 'install_log_dir' => 'install-logs',
+ 'sanity_check' => 1,
+ },
+ '_mirror' => {
+ 'base' => 'authors/id/',
+ 'auth' => 'authors/01mailrc.txt.gz',
+ 'dslip' => 'modules/03modlist.data.gz',
+ 'mod' => 'modules/02packages.details.txt.gz'
+ },
+};
+
+sub new {
+ my $class = shift;
+ my $obj = $class->SUPER::new;
+
+ $obj->mk_accessors( keys %$Conf );
+
+ for my $acc ( keys %$Conf ) {
+ my $subobj = Object::Accessor->new;
+ $subobj->mk_accessors( keys %{$Conf->{$acc}} );
+
+ ### read in all the settings from the sub accessors;
+ for my $subacc ( $subobj->ls_accessors ) {
+ $subobj->$subacc( $Conf->{$acc}->{$subacc} );
+ }
+
+ ### now store it in the parent object
+ $obj->$acc( $subobj );
+ }
+
+ $obj->_clean_up_paths;
+
+ ### shut up IPC::Cmd warning about not findin IPC::Run on win32
+ $IPC::Cmd::WARN = 0;
+
+ return $obj;
+}
+
+sub _clean_up_paths {
+ my $self = shift;
+
+ ### clean up paths if we are on win32
+ if( $^O eq 'MSWin32' ) {
+ for my $pgm ( $self->program->ls_accessors ) {
+ $self->program->$pgm(
+ Win32::GetShortPathName( $self->program->$pgm )
+ ) if $self->program->$pgm =~ /\s+/;
+ }
+ }
+
+ return 1;
+}
+
+1;
diff --git a/lib/CPANPLUS/Configure.pm b/lib/CPANPLUS/Configure.pm
new file mode 100644
index 0000000000..51d74ef4db
--- /dev/null
+++ b/lib/CPANPLUS/Configure.pm
@@ -0,0 +1,601 @@
+package CPANPLUS::Configure;
+use strict;
+
+
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Error;
+use CPANPLUS::Config;
+
+use Log::Message;
+use Module::Load qw[load];
+use Params::Check qw[check];
+use File::Basename qw[dirname];
+use Module::Loaded ();
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+use vars qw[$AUTOLOAD $VERSION $MIN_CONFIG_VERSION];
+use base qw[CPANPLUS::Internals::Utils];
+
+local $Params::Check::VERBOSE = 1;
+
+### require, avoid circular use ###
+require CPANPLUS::Internals;
+$VERSION = $CPANPLUS::Internals::VERSION = $CPANPLUS::Internals::VERSION;
+
+### can't use O::A as we're using our own AUTOLOAD to get to
+### the config options.
+for my $meth ( qw[conf]) {
+ no strict 'refs';
+
+ *$meth = sub {
+ my $self = shift;
+ $self->{'_'.$meth} = $_[0] if @_;
+ return $self->{'_'.$meth};
+ }
+}
+
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Configure
+
+=head1 SYNOPSIS
+
+ $conf = CPANPLUS::Configure->new( );
+
+ $bool = $conf->can_save;
+ $bool = $conf->save( $where );
+
+ @opts = $conf->options( $type );
+
+ $make = $conf->get_program('make');
+ $verbose = $conf->set_conf( verbose => 1 );
+
+=head1 DESCRIPTION
+
+This module deals with all the configuration issues for CPANPLUS.
+Users can use objects created by this module to alter the behaviour
+of CPANPLUS.
+
+Please refer to the C<CPANPLUS::Backend> documentation on how to
+obtain a C<CPANPLUS::Configure> object.
+
+=head1 METHODS
+
+=head2 $Configure = CPANPLUS::Configure->new( load_configs => BOOL )
+
+This method returns a new object. Normal users will never need to
+invoke the C<new> method, but instead retrieve the desired object via
+a method call on a C<CPANPLUS::Backend> object.
+
+The C<load_configs> parameter controls wether or not additional
+user configurations are to be loaded or not. Defaults to C<true>.
+
+=cut
+
+### store teh CPANPLUS::Config object in a closure, so we only
+### initialize it once.. otherwise, on a 2nd ->new, settings
+### from configs on top of this one will be reset
+{ my $Config;
+
+ sub new {
+ my $class = shift;
+ my %hash = @_;
+
+ ### XXX pass on options to ->init() like rescan?
+ my ($load);
+ my $tmpl = {
+ load_configs => { default => 1, store => \$load },
+ };
+
+ check( $tmpl, \%hash ) or (
+ warn Params::Check->last_error, return
+ );
+
+ $Config ||= CPANPLUS::Config->new;
+ my $self = bless {}, $class;
+ $self->conf( $Config );
+
+ ### you want us to load other configs?
+ ### these can override things in the default config
+ $self->init if $load;
+
+ return $self;
+ }
+}
+
+=head2 $bool = $Configure->init( [rescan => BOOL])
+
+Initialize the configure with other config files than just
+the default 'CPANPLUS::Config'.
+
+Called from C<new()> to load user/system configurations
+
+If the C<rescan> option is provided, your disk will be
+examined again to see if there are new config files that
+could be read. Defaults to C<false>.
+
+Returns true on success, false on failure.
+
+=cut
+
+### move the Module::Pluggable detection to runtime, rather
+### than compile time, so that a simple 'require CPANPLUS'
+### doesn't start running over your filesystem for no good
+### reason. Make sure we only do the M::P call once though.
+### we use $loaded to mark it
+{ my $loaded;
+ my $warned;
+ sub init {
+ my $self = shift;
+ my $obj = $self->conf;
+ my %hash = @_;
+
+ my ($rescan);
+ my $tmpl = {
+ rescan => { default => 0, store => \$rescan },
+ };
+
+ check( $tmpl, \%hash ) or (
+ warn Params::Check->last_error, return
+ );
+
+ ### warn if we find an old style config specified
+ ### via environment variables
+ { my $env = ENV_CPANPLUS_CONFIG;
+ if( $ENV{$env} and not $warned ) {
+ $warned++;
+ error(loc("Specifying a config file in your environment " .
+ "using %1 is obsolete.\nPlease follow the ".
+ "directions outlined in %2 or use the '%3' command\n".
+ "in the default shell to use custom config files.",
+ $env, "CPANPLUS::Configure->save", 's save'));
+ }
+ }
+
+ ### make sure that the homedir is included now
+ local @INC = ( CONFIG_USER_LIB_DIR->(), @INC );
+
+ ### only set it up once
+ if( !$loaded++ or $rescan ) {
+ ### find plugins & extra configs
+ ### check $home/.cpanplus/lib as well
+ require Module::Pluggable;
+
+ Module::Pluggable->import(
+ search_path => ['CPANPLUS::Config'],
+ search_dirs => [ CONFIG_USER_LIB_DIR ],
+ except => qr/::SUPER$/,
+ sub_name => 'configs'
+ );
+ }
+
+
+ ### do system config, user config, rest.. in that order
+ ### apparently, on a 2nd invocation of -->configs, a
+ ### ::ISA::CACHE package can appear.. that's bad...
+ my %confs = map { $_ => $_ }
+ grep { $_ !~ /::ISA::/ } __PACKAGE__->configs;
+ my @confs = grep { defined }
+ map { delete $confs{$_} } CONFIG_SYSTEM, CONFIG_USER;
+ push @confs, sort keys %confs;
+
+ for my $plugin ( @confs ) {
+ msg(loc("Found config '%1'", $plugin),0);
+
+ ### if we already did this the /last/ time around dont
+ ### run the setup agian.
+ if( my $loc = Module::Loaded::is_loaded( $plugin ) ) {
+ msg(loc(" Already loaded '%1' (%2)", $plugin, $loc), 0);
+ next;
+ } else {
+ msg(loc(" Loading config '%1'", $plugin),0);
+
+ eval { load $plugin };
+ msg(loc(" Loaded '%1' (%2)",
+ $plugin, Module::Loaded::is_loaded( $plugin ) ), 0);
+ }
+
+ if( $@ ) {
+ error(loc("Could not load '%1': %2", $plugin, $@));
+ next;
+ }
+
+ my $sub = $plugin->can('setup');
+ $sub->( $self ) if $sub;
+ }
+
+ ### clean up the paths once more, just in case
+ $obj->_clean_up_paths;
+
+ return 1;
+ }
+}
+=pod
+
+=head2 can_save( [$config_location] )
+
+Check if we can save the configuration to the specified file.
+If no file is provided, defaults to your personal config.
+
+Returns true if the file can be saved, false otherwise.
+
+=cut
+
+sub can_save {
+ my $self = shift;
+ my $file = shift || CONFIG_USER_FILE->();
+
+ return 1 unless -e $file;
+
+ chmod 0644, $file;
+ return (-w $file);
+}
+
+=pod
+
+=head2 $file = $conf->save( [$package_name] )
+
+Saves the configuration to the package name you provided.
+If this package is not C<CPANPLUS::Config::System>, it will
+be saved in your C<.cpanplus> directory, otherwise it will
+be attempted to be saved in the system wide directory.
+
+If no argument is provided, it will default to your personal
+config.
+
+Returns the full path to the file if the config was saved,
+false otherwise.
+
+=cut
+
+sub _config_pm_to_file {
+ my $self = shift;
+ my $pm = shift or return;
+ my $dir = shift || CONFIG_USER_LIB_DIR->();
+
+ ### only 3 types of files know: home, system and 'other'
+ ### so figure out where to save them based on their type
+ my $file;
+ if( $pm eq CONFIG_USER ) {
+ $file = CONFIG_USER_FILE->();
+
+ } elsif ( $pm eq CONFIG_SYSTEM ) {
+ $file = CONFIG_SYSTEM_FILE->();
+
+ ### third party file
+ } else {
+ my $cfg_pkg = CONFIG . '::';
+ unless( $pm =~ /^$cfg_pkg/ ) {
+ error(loc(
+ "WARNING: Your config package '%1' is not in the '%2' ".
+ "namespace and will not be automatically detected by %3",
+ $pm, $cfg_pkg, 'CPANPLUS'
+ ));
+ }
+
+ $file = File::Spec->catfile(
+ $dir,
+ split( '::', $pm )
+ ) . '.pm';
+ }
+
+ return $file;
+}
+
+
+sub save {
+ my $self = shift;
+ my $pm = shift || CONFIG_USER;
+ my $savedir = shift || '';
+
+ my $file = $self->_config_pm_to_file( $pm, $savedir ) or return;
+ my $dir = dirname( $file );
+
+ unless( -d $dir ) {
+ $self->_mkdir( dir => $dir ) or (
+ error(loc("Can not create directory '%1' to save config to",$dir)),
+ return
+ )
+ }
+ return unless $self->can_save($file);
+
+ ### find only accesors that are not private
+ my @acc = sort grep { $_ !~ /^_/ } $self->conf->ls_accessors;
+
+ ### for dumping the values
+ use Data::Dumper;
+
+ my @lines;
+ for my $acc ( @acc ) {
+
+ push @lines, "### $acc section", $/;
+
+ for my $key ( $self->conf->$acc->ls_accessors ) {
+ my $val = Dumper( $self->conf->$acc->$key );
+
+ $val =~ s/\$VAR1\s+=\s+//;
+ $val =~ s/;\n//;
+
+ push @lines, '$'. "conf->set_${acc}( $key => $val );", $/;
+ }
+ push @lines, $/,$/;
+
+ }
+
+ my $str = join '', map { " $_" } @lines;
+
+ ### use a variable to make sure the pod parser doesn't snag it
+ my $is = '=';
+ my $time = gmtime;
+
+
+ my $msg = <<_END_OF_CONFIG_;
+###############################################
+###
+### Configuration structure for $pm
+###
+###############################################
+
+#last changed: $time GMT
+
+### minimal pod, so you can find it with perldoc -l, etc
+${is}pod
+
+${is}head1 NAME
+
+$pm
+
+${is}head1 DESCRIPTION
+
+This is a CPANPLUS configuration file. Editing this
+config changes the way CPANPLUS will behave
+
+${is}cut
+
+package $pm;
+
+use strict;
+
+sub setup {
+ my \$conf = shift;
+
+$str
+
+ return 1;
+}
+
+1;
+
+_END_OF_CONFIG_
+
+ $self->_move( file => $file, to => "$file~" ) if -f $file;
+
+ my $fh = new FileHandle;
+ $fh->open(">$file")
+ or (error(loc("Could not open '%1' for writing: %2", $file, $!)),
+ return );
+
+ $fh->print($msg);
+ $fh->close;
+
+ return $file;
+}
+
+=pod
+
+=head2 options( type => TYPE )
+
+Returns a list of all valid config options given a specific type
+(like for example C<conf> of C<program>) or false if the type does
+not exist
+
+=cut
+
+sub options {
+ my $self = shift;
+ my $conf = $self->conf;
+ my %hash = @_;
+
+ my $type;
+ my $tmpl = {
+ type => { required => 1, default => '',
+ strict_type => 1, store => \$type },
+ };
+
+ check($tmpl, \%hash) or return;
+
+ my %seen;
+ return sort grep { !$seen{$_}++ }
+ map { $_->$type->ls_accessors if $_->can($type) }
+ $self->conf;
+ return;
+}
+
+=pod
+
+=head1 ACCESSORS
+
+Accessors that start with a C<_> are marked private -- regular users
+should never need to use these.
+
+=head2 get_SOMETHING( ITEM, [ITEM, ITEM, ... ] );
+
+The C<get_*> style accessors merely retrieves one or more desired
+config options.
+
+=head2 set_SOMETHING( ITEM => VAL, [ITEM => VAL, ITEM => VAL, ... ] );
+
+The C<set_*> style accessors set the current value for one
+or more config options and will return true upon success, false on
+failure.
+
+=head2 add_SOMETHING( ITEM => VAL, [ITEM => VAL, ITEM => VAL, ... ] );
+
+The C<add_*> style accessor adds a new key to a config key.
+
+Currently, the following accessors exist:
+
+=over 4
+
+=item set|get_conf
+
+Simple configuration directives like verbosity and favourite shell.
+
+=item set|get_program
+
+Location of helper programs.
+
+=item _set|_get_build
+
+Locations of where to put what files for CPANPLUS.
+
+=item _set|_get_source
+
+Locations and names of source files locally.
+
+=item _set|_get_mirror
+
+Locations and names of source files remotely.
+
+=item _set|_get_dist
+
+Mapping of distribution format names to modules.
+
+=item _set|_get_fetch
+
+Special settings pertaining to the fetching of files.
+
+=item _set|_get_daemon
+
+Settings for C<cpanpd>, the CPANPLUS daemon.
+
+=back
+
+=cut
+
+sub AUTOLOAD {
+ my $self = shift;
+ my $conf = $self->conf;
+
+ my $name = $AUTOLOAD;
+ $name =~ s/.+:://;
+
+ my ($private, $action, $field) =
+ $name =~ m/^(_)?((?:[gs]et|add))_([a-z]+)$/;
+
+ my $type = '';
+ $type .= '_' if $private;
+ $type .= $field if $field;
+
+ unless ( $conf->can($type) ) {
+ error( loc("Invalid method type: '%1'", $name) );
+ return;
+ }
+
+ unless( scalar @_ ) {
+ error( loc("No arguments provided!") );
+ return;
+ }
+
+ ### retrieve a current value for an existing key ###
+ if( $action eq 'get' ) {
+ for my $key (@_) {
+ my @list = ();
+
+ ### get it from the user config first
+ if( $conf->can($type) and $conf->$type->can($key) ) {
+ push @list, $conf->$type->$key;
+
+ ### XXX EU::AI compatibility hack to provide lookups like in
+ ### cpanplus 0.04x; we renamed ->_get_build('base') to
+ ### ->get_conf('base')
+ } elsif ( $type eq '_build' and $key eq 'base' ) {
+ return $self->get_conf($key);
+
+ } else {
+ error( loc(q[No such key '%1' in field '%2'], $key, $type) );
+ return;
+ }
+
+ return wantarray ? @list : $list[0];
+ }
+
+ ### set an existing key to a new value ###
+ } elsif ( $action eq 'set' ) {
+ my %args = @_;
+
+ while( my($key,$val) = each %args ) {
+
+ if( $conf->can($type) and $conf->$type->can($key) ) {
+ $conf->$type->$key( $val );
+
+ } else {
+ error( loc(q[No such key '%1' in field '%2'], $key, $type) );
+ return;
+ }
+ }
+
+ return 1;
+
+ ### add a new key to the config ###
+ } elsif ( $action eq 'add' ) {
+ my %args = @_;
+
+ while( my($key,$val) = each %args ) {
+
+ if( $conf->$type->can($key) ) {
+ error( loc( q[Key '%1' already exists for field '%2'],
+ $key, $type));
+ return;
+ } else {
+ $conf->$type->mk_accessors( $key );
+ $conf->$type->$key( $val );
+ }
+ }
+ return 1;
+
+ } else {
+
+ error( loc(q[Unknown action '%1'], $action) );
+ return;
+ }
+}
+
+sub DESTROY { 1 };
+
+1;
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Backend>, L<CPANPLUS::Configure::Setup>
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/Configure/Setup.pm b/lib/CPANPLUS/Configure/Setup.pm
new file mode 100644
index 0000000000..81ee2cadfe
--- /dev/null
+++ b/lib/CPANPLUS/Configure/Setup.pm
@@ -0,0 +1,1628 @@
+package CPANPLUS::Configure::Setup;
+
+use strict;
+use vars qw(@ISA);
+
+use base qw[CPANPLUS::Internals::Utils];
+use base qw[Object::Accessor];
+
+use Config;
+use Term::UI;
+use Module::Load;
+use Term::ReadLine;
+
+
+use CPANPLUS::Internals::Utils;
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Error;
+
+use IPC::Cmd qw[can_run];
+use Params::Check qw[check];
+use Module::Load::Conditional qw[check_install];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+### silence Term::UI
+$Term::UI::VERBOSE = 0;
+
+#Can't ioctl TIOCGETP: Unknown error
+#Consider installing Term::ReadKey from CPAN site nearby
+# at http://www.perl.com/CPAN
+#Or use
+# perl -MCPAN -e shell
+#to reach CPAN. Falling back to 'stty'.
+# If you do not want to see this warning, set PERL_READLINE_NOWARN
+#in your environment.
+#'stty' is not recognized as an internal or external command,
+#operable program or batch file.
+#Cannot call `stty': No such file or directory at C:/Perl/site/lib/Term/ReadLine/
+
+### setting this var in the meantime to avoid this warning ###
+$ENV{PERL_READLINE_NOWARN} = 1;
+
+
+sub new {
+ my $class = shift;
+ my %hash = @_;
+
+ my $tmpl = {
+ configure_object => { },
+ term => { },
+ backend => { },
+ autoreply => { default => 0, },
+ skip_mirrors => { default => 0, },
+ use_previous => { default => 1, },
+ config_type => { default => CONFIG_USER },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### initialize object
+ my $obj = $class->SUPER::new( keys %$tmpl );
+ for my $acc ( $obj->ls_accessors ) {
+ $obj->$acc( $args->{$acc} );
+ }
+
+ ### otherwise there's a circular use ###
+ load CPANPLUS::Configure;
+ load CPANPLUS::Backend;
+
+ $obj->configure_object( CPANPLUS::Configure->new() )
+ unless $obj->configure_object;
+
+ $obj->backend( CPANPLUS::Backend->new( $obj->configure_object ) )
+ unless $obj->backend;
+
+ ### use empty string in case user only has T::R::Stub -- it complains
+ $obj->term( Term::ReadLine->new('') )
+ unless $obj->term;
+
+ ### enable autoreply if that was passed ###
+ $Term::UI::AUTOREPLY = $obj->autoreply;
+
+ return $obj;
+}
+
+sub init {
+ my $self = shift;
+ my $term = $self->term;
+
+ ### default setting, unless changed
+ $self->config_type( CONFIG_USER ) unless $self->config_type;
+
+ my $save = loc('Save & exit');
+ my $exit = loc('Quit without saving');
+ my @map = (
+ # key on the display # method to dispatch to
+ [ loc('Select Configuration file') => '_save_where' ],
+ [ loc('Setup CLI Programs') => '_setup_program' ],
+ [ loc('Setup CPANPLUS Home directory') => '_setup_base' ],
+ [ loc('Setup FTP/Email settings') => '_setup_ftp' ],
+ [ loc('Setup basic preferences') => '_setup_conf' ],
+ [ loc('Setup installer settings') => '_setup_installer' ],
+ [ loc('Select mirrors'), => '_setup_hosts' ],
+ [ loc('Edit configuration file') => '_edit' ],
+ [ $save => '_save' ],
+ [ $exit => 1 ],
+ );
+
+ my @keys = map { $_->[0] } @map; # sorted keys
+ my %map = map { @$_ } @map; # lookup hash
+
+ PICK_SECTION: {
+ print loc("
+=================> MAIN MENU <=================
+
+Welcome to the CPANPLUS configuration. Please select which
+parts you wish to configure
+
+Defaults are taken from your current configuration.
+If you would save now, your settings would be written to:
+
+ %1
+
+ ", $self->config_type );
+
+ my $choice = $term->get_reply(
+ prompt => "Section to configure:",
+ choices => \@keys,
+ default => $keys[0]
+ );
+
+ ### exit configuration?
+ if( $choice eq $exit ) {
+ print loc("
+Quitting setup, changes will not be saved.
+ ");
+ return 1;
+ }
+
+ my $method = $map{$choice};
+
+ my $rv = $self->$method or print loc("
+There was an error setting up this section. You might want to try again
+ ");
+
+ ### was it save & exit?
+ if( $choice eq $save and $rv ) {
+ print loc("
+Quitting setup, changes are saved to '%1'
+ ", $self->config_type
+ );
+ return 1;
+ }
+
+ ### otherwise, present choice again
+ redo PICK_SECTION;
+ }
+
+ return 1;
+}
+
+
+
+### sub that figures out what kind of config type the user wants
+sub _save_where {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->configure_object;
+
+
+ ASK_CONFIG_TYPE: {
+
+ print loc( q[
+Where would you like to save your CPANPLUS Configuration file?
+
+If you want to configure CPANPLUS for this user only,
+select the '%1' option.
+The file will then be saved in your homedirectory.
+
+If you are the system administrator of this machine,
+and would like to make this config available globally,
+select the '%2' option.
+The file will be then be saved in your CPANPLUS
+installation directory.
+
+ ], CONFIG_USER, CONFIG_SYSTEM );
+
+
+ ### ask what config type we should save to
+ my $type = $term->get_reply(
+ prompt => loc("Type of configuration file"),
+ default => $self->config_type || CONFIG_USER,
+ choices => [CONFIG_USER, CONFIG_SYSTEM],
+ );
+
+ my $file = $conf->_config_pm_to_file( $type );
+
+ ### can we save to this file?
+ unless( $conf->can_save( $file ) ) {
+ error(loc(
+ "Can not save to file '%1'-- please check permissions " .
+ "and try again", $file
+ ));
+
+ redo ASK_CONFIG_FILE;
+ }
+
+ ### you already have the file -- are we allowed to overwrite
+ ### or should we try again?
+ if ( -e $file and -w _ ) {
+ print loc(q[
+I see you already have this file:
+ %1
+
+If you continue & save this file, the previous version will be overwritten.
+
+ ], $file );
+
+ redo ASK_CONFIG_TYPE
+ unless $term->ask_yn(
+ prompt => loc( "Shall I overwrite it?"),
+ default => 'n',
+ );
+ }
+
+ print $/, loc("Using '%1' as your configuration type", $type);
+
+ return $self->config_type($type);
+ }
+}
+
+
+### setup the build & cache dirs
+sub _setup_base {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->configure_object;
+
+ my $base = $conf->get_conf('base');
+ my $home = File::Spec->catdir( $self->_home_dir, DOT_CPANPLUS );
+
+ print loc("
+CPANPLUS needs a directory of its own to cache important index
+files and maybe keep a temporary mirror of CPAN files.
+This may be a site-wide directory or a personal directory.
+
+For a single-user installation, we suggest using your home directory.
+
+");
+
+ my $where;
+ ASK_HOME_DIR: {
+ my $other = loc('Somewhere else');
+ if( $base and ($base ne $home) ) {
+ print loc("You have several choices:");
+
+ $where = $term->get_reply(
+ prompt => loc('Please pick one'),
+ choices => [$home, $base, $other],
+ default => $home,
+ );
+ } else {
+ $where = $base;
+ }
+
+ if( $where and -d $where ) {
+ print loc("
+I see you already have a directory:
+ %1
+
+ "), $where;
+
+ my $yn = $term->ask_yn(
+ prompt => loc('Should I use it?'),
+ default => 'y',
+ );
+ $where = '' unless $yn;
+ }
+
+ if( $where and ($where ne $other) and not -d $where ) {
+ if (!$self->_mkdir( dir => $where ) ) {
+ print "\n", loc("Unable to create directory '%1'", $where);
+ redo ASK_HOME_DIR;
+ }
+
+ } elsif( not $where or ($where eq $other) ) {
+ print loc("
+First of all, I'd like to create this directory.
+
+ ");
+
+ NEW_HOME: {
+ $where = $term->get_reply(
+ prompt => loc('Where shall I create it?'),
+ default => $home,
+ );
+
+ my $again;
+ if( -d $where and not -w _ ) {
+ print "\n", loc("I can't seem to write in this directory");
+ $again++;
+ } elsif (!$self->_mkdir( dir => $where ) ) {
+ print "\n", loc("Unable to create directory '%1'", $where);
+ $again++;
+ }
+
+ if( $again ) {
+ print "\n", loc('Please select another directory'), "\n\n";
+ redo NEW_HOME;
+ }
+ }
+ }
+ }
+
+ ### tidy up the path and store it
+ $where = File::Spec->rel2abs($where);
+ $conf->set_conf( base => $where );
+
+ ### create subdirectories ###
+ my @dirs =
+ File::Spec->catdir( $where, $self->_perl_version(perl => $^X),
+ $conf->_get_build('moddir') ),
+ map {
+ File::Spec->catdir( $where, $conf->_get_build($_) )
+ } qw[autdir distdir];
+
+ for my $dir ( @dirs ) {
+ unless( $self->_mkdir( dir => $dir ) ) {
+ warn loc("I wasn't able to create '%1'", $dir), "\n";
+ }
+ }
+
+ ### clear away old storable images before 0.031
+ for my $src (qw[dslip mailrc packages]) {
+ 1 while unlink File::Spec->catfile( $where, $src );
+
+ }
+
+ print loc(q[
+Your CPANPLUS build and cache directory has been set to:
+ %1
+
+ ], $where);
+
+ return 1;
+}
+
+sub _setup_ftp {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->configure_object;
+
+ #########################
+ ## are you a pacifist? ##
+ #########################
+
+ print loc("
+If you are connecting through a firewall or proxy that doesn't handle
+FTP all that well you can use passive FTP.
+
+");
+
+ my $yn = $term->ask_yn(
+ prompt => loc("Use passive FTP?"),
+ default => $conf->get_conf('passive'),
+ );
+
+ $conf->set_conf(passive => $yn);
+
+ ### set the ENV var as well, else it won't get set till AFTER
+ ### the configuration is saved. but we fetch files BEFORE that.
+ $ENV{FTP_PASSIVE} = $yn;
+
+ print "\n";
+ print $yn
+ ? loc("I will use passive FTP.")
+ : loc("I won't use passive FTP.");
+ print "\n";
+
+ #############################
+ ## should fetches timeout? ##
+ #############################
+
+ print loc("
+CPANPLUS can specify a network timeout for downloads (in whole seconds).
+If none is desired (or to skip this question), enter '0'.
+
+");
+
+ my $timeout = 0 + $term->get_reply(
+ prompt => loc("Network timeout for downloads"),
+ default => $conf->get_conf('timeout') || 0,
+ allow => qr/(?!\D)/, ### whole numbers only
+ );
+
+ $conf->set_conf(timeout => $timeout);
+
+ print "\n";
+ print $timeout
+ ? loc("The network timeout for downloads is %1 seconds.", $timeout)
+ : loc("The network timeout for downloads is not set.");
+ print "\n";
+
+ ############################
+ ## where can I reach you? ##
+ ############################
+
+ print loc("
+What email address should we send as our anonymous password when
+fetching modules from CPAN servers? Some servers will NOT allow you to
+connect without a valid email address, or at least something that looks
+like one.
+Also, if you choose to report test results at some point, a valid email
+is required for the 'from' field, so choose wisely.
+
+ ");
+
+ my $other = 'Something else';
+ my @choices = (DEFAULT_EMAIL, $Config{cf_email}, $other);
+ my $current = $conf->get_conf('email');
+
+ ### if your current address is not in the list, add it to the choices
+ unless (grep { $_ eq $current } @choices) {
+ unshift @choices, $current;
+ }
+
+ my $email = $term->get_reply(
+ prompt => loc('Which email address shall I use?'),
+ default => $current || $choices[0],
+ choices => \@choices,
+ );
+
+ if( $email eq $other ) {
+ EMAIL: {
+ $email = $term->get_reply(
+ prompt => loc('Email address: '),
+ );
+
+ unless( $self->_valid_email($email) ) {
+ print loc("
+You did not enter a valid email address, please try again!
+ ") if length $email;
+
+ redo EMAIL;
+ }
+ }
+ }
+
+ print loc("
+Your 'email' is now:
+ %1
+
+ ", $email);
+
+ $conf->set_conf( email => $email );
+
+ return 1;
+}
+
+
+### commandline programs
+sub _setup_program {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->configure_object;
+
+ print loc("
+CPANPLUS can use command line utilities to do certain
+tasks, rather than use perl modules.
+
+If you wish to use a certain command utility, just enter
+the full path (or accept the default). If you do not wish
+to use it, enter a single space.
+
+Note that the paths you provide should not contain spaces, which is
+needed to make a distinction between program name and options to that
+program. For Win32 machines, you can use the short name for a path,
+like '%1'.
+
+ ", 'c:\Progra~1\prog.exe' );
+
+ for my $prog ( sort $conf->options( type => 'program') ) {
+ PROGRAM: {
+ print loc("Where can I find your '%1' utility? ".
+ "(Enter a single space to disable)", $prog );
+
+ my $loc = $term->get_reply(
+ prompt => "Path to your '$prog'",
+ default => $conf->get_program( $prog ),
+ );
+
+ ### empty line clears it
+ my $cmd = $loc =~ /^\s*$/ ? undef : $loc;
+ my ($bin) = $cmd =~ /^(\S+)/;
+
+ ### did you provide a valid program ?
+ if( $bin and not can_run( $bin ) ) {
+ print "\n";
+ print loc("Can not find the binary '%1' in your path!", $bin);
+ redo PROGRAM;
+ }
+
+ ### make is special -- we /need/ it!
+ if( $prog eq 'make' and not $bin ) {
+ print loc(
+ "==> Without your '%1' utility, I can not function! <==",
+ 'make'
+ );
+ print loc("Please provide one!");
+
+ ### show win32 where to download
+ if ( $^O eq 'MSWin32' ) {
+ print loc("You can get '%1' from:", NMAKE);
+ print "\t". NMAKE_URL ."\n";
+ }
+ print "\n";
+ redo PROGRAM;
+ }
+
+ $conf->set_program( $prog => $cmd );
+ print $cmd
+ ? loc( "Your '%1' utility has been set to '%2'",
+ $prog, $cmd )
+ : loc( "Your '%1' has been disabled", $prog );
+ print "\n";
+ }
+ }
+
+ return 1;
+}
+
+sub _setup_installer {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->configure_object;
+
+ my $none = 'None';
+ {
+ print loc("
+CPANPLUS uses binary programs as well as Perl modules to accomplish
+various tasks. Normally, CPANPLUS will prefer the use of Perl modules
+over binary programs.
+
+You can change this setting by making CPANPLUS prefer the use of
+certain binary programs if they are available.
+
+ ");
+
+ ### default to using binaries if we don't have compress::zlib only
+ ### -- it'll get very noisy otherwise
+ my $type = 'prefer_bin';
+ my $yn = $term->ask_yn(
+ prompt => loc("Should I prefer the use of binary programs?"),
+ default => $conf->get_conf( $type ),
+ );
+
+ print $yn
+ ? loc("Ok, I will prefer to use binary programs if possible.")
+ : loc("Ok, I will prefer to use Perl modules if possible.");
+ print "\n\n";
+
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ {
+ print loc("
+Makefile.PL is run by perl in a separate process, and accepts various
+flags that controls the module's installation. For instance, if you
+would like to install modules to your private user directory, set
+'makemakerflags' to:
+
+LIB=~/perl/lib INSTALLMAN1DIR=~/perl/man/man1 INSTALLMAN3DIR=~/perl/man/man3
+
+and be sure that you do NOT set UNINST=1 in 'makeflags' below.
+
+Enter a name=value list separated by whitespace, but quote any embedded
+spaces that you want to preserve. (Enter a space to clear any existing
+settings.)
+
+If you don't understand this question, just press ENTER.
+
+ ");
+
+ my $type = 'makemakerflags';
+ my $flags = $term->get_reply(
+ prompt => 'Makefile.PL flags?',
+ default => $conf->get_conf($type),
+ );
+
+ $flags = '' if $flags eq $none || $flags !~ /\S/;
+
+ print "\n", loc("Your '%1' have been set to:", 'Makefile.PL flags'),
+ "\n ", ( $flags ? $flags : loc('*nothing entered*')),
+ "\n\n";
+
+ $conf->set_conf( $type => $flags );
+ }
+
+ {
+ print loc("
+Like Makefile.PL, we run 'make' and 'make install' as separate processes.
+If you have any parameters (e.g. '-j3' in dual processor systems) you want
+to pass to the calls, please specify them here.
+
+In particular, 'UNINST=1' is recommended for root users, unless you have
+fine-tuned ideas of where modules should be installed in the \@INC path.
+
+Enter a name=value list separated by whitespace, but quote any embedded
+spaces that you want to preserve. (Enter a space to clear any existing
+settings.)
+
+Again, if you don't understand this question, just press ENTER.
+
+ ");
+ my $type = 'makeflags';
+ my $flags = $term->get_reply(
+ prompt => 'make flags?',
+ default => $conf->get_conf($type),
+ );
+
+ $flags = '' if $flags eq $none || $flags !~ /\S/;
+
+ print "\n", loc("Your '%1' have been set to:", $type),
+ "\n ", ( $flags ? $flags : loc('*nothing entered*')),
+ "\n\n";
+
+ $conf->set_conf( $type => $flags );
+ }
+
+ {
+ print loc("
+An alternative to ExtUtils::MakeMaker and Makefile.PL there's a module
+called Module::Build which uses a Build.PL.
+
+If you would like to specify any flags to pass when executing the
+Build.PL (and Build) script, please enter them below.
+
+For instance, if you would like to install modules to your private
+user directory, you could enter:
+
+ install_base=/my/private/path
+
+Or to uninstall old copies of modules before updating, you might
+want to enter:
+
+ uninst=1
+
+Again, if you don't understand this question, just press ENTER.
+
+ ");
+
+ my $type = 'buildflags';
+ my $flags = $term->get_reply(
+ prompt => 'Build.PL and Build flags?',
+ default => $conf->get_conf($type),
+ );
+
+ $flags = '' if $flags eq $none || $flags !~ /\S/;
+
+ print "\n", loc("Your '%1' have been set to:",
+ 'Build.PL and Build flags'),
+ "\n ", ( $flags ? $flags : loc('*nothing entered*')),
+ "\n\n";
+
+ $conf->set_conf( $type => $flags );
+ }
+
+ ### use EU::MM or module::build? ###
+ {
+ print loc("
+Some modules provide both a Build.PL (Module::Build) and a Makefile.PL
+(ExtUtils::MakeMaker). By default, CPANPLUS prefers Makefile.PL.
+
+Module::Build support is not bundled standard with CPANPLUS, but
+requires you to install 'CPANPLUS::Dist::Build' from CPAN.
+
+Although Module::Build is a pure perl solution, which means you will
+not need a 'make' binary, it does have some limitations. The most
+important is that CPANPLUS is unable to uninstall any modules installed
+by Module::Build.
+
+Again, if you don't understand this question, just press ENTER.
+
+ ");
+ my $type = 'prefer_makefile';
+ my $yn = $term->ask_yn(
+ prompt => loc("Prefer Makefile.PL over Build.PL?"),
+ default => $conf->get_conf($type),
+ );
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ {
+ print loc('
+If you like, CPANPLUS can add extra directories to your @INC list during
+startup. These will just be used by CPANPLUS and will not change your
+external environment or perl interpreter. Enter a space separated list of
+pathnames to be added to your @INC, quoting any with embedded whitespace.
+(To clear the current value enter a single space.)
+
+ ');
+
+ my $type = 'lib';
+ my $flags = $term->get_reply(
+ prompt => loc('Additional @INC directories to add?'),
+ default => (join " ", @{$conf->get_conf($type) || []} ),
+ );
+
+ my $lib;
+ unless( $flags =~ /\S/ ) {
+ $lib = [];
+ } else {
+ (@$lib) = $flags =~ m/\s*("[^"]+"|'[^']+'|[^\s]+)/g;
+ }
+
+ print "\n", loc("Your additional libs are now:"), "\n";
+
+ print scalar @$lib
+ ? map { " $_\n" } @$lib
+ : " ", loc("*nothing entered*"), "\n";
+ print "\n\n";
+
+ $conf->set_conf( $type => $lib );
+ }
+
+ return 1;
+}
+
+
+sub _setup_conf {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->configure_object;
+
+ my $none = 'None';
+ {
+ ############
+ ## noisy? ##
+ ############
+
+ print loc("
+In normal operation I can just give you basic information about what I
+am doing, or I can be more verbose and give you every little detail.
+
+ ");
+
+ my $type = 'verbose';
+ my $yn = $term->ask_yn(
+ prompt => loc("Should I be verbose?"),
+ default => $conf->get_conf( $type ), );
+
+ print "\n";
+ print $yn
+ ? loc("You asked for it!")
+ : loc("I'll try to be quiet");
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ {
+ #######################
+ ## flush you animal! ##
+ #######################
+
+ print loc("
+In the interest of speed, we keep track of what modules were installed
+successfully and which failed in the current session. We can flush this
+data automatically, or you can explicitly issue a 'flush' when you want
+to purge it.
+
+ ");
+
+ my $type = 'flush';
+ my $yn = $term->ask_yn(
+ prompt => loc("Flush automatically?"),
+ default => $conf->get_conf( $type ),
+ );
+
+ print "\n";
+ print $yn
+ ? loc("I'll flush after every full module install.")
+ : loc("I won't flush until you tell me to.");
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ {
+ #####################
+ ## force installs? ##
+ #####################
+
+ print loc("
+Usually, when a test fails, I won't install the module, but if you
+prefer, I can force the install anyway.
+
+ ");
+
+ my $type = 'force';
+ my $yn = $term->ask_yn(
+ prompt => loc("Force installs?"),
+ default => $conf->get_conf( $type ),
+ );
+
+ print "\n";
+ print $yn
+ ? loc("I will force installs.")
+ : loc("I won't force installs.");
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ {
+ ###################
+ ## about prereqs ##
+ ###################
+
+ print loc("
+Sometimes a module will require other modules to be installed before it
+will work. CPANPLUS can attempt to install these for you automatically
+if you like, or you can do the deed yourself.
+
+If you would prefer that we NEVER try to install extra modules
+automatically, select NO. (Usually you will want this set to YES.)
+
+If you would like to build modules to satisfy testing or prerequisites,
+but not actually install them, select BUILD.
+
+NOTE: This feature requires you to flush the 'lib' cache for longer
+running programs (refer to the CPANPLUS::Backend documentations for
+more details).
+
+Otherwise, select ASK to have us ask your permission to install them.
+
+ ");
+
+ my $type = 'prereqs';
+
+ my @map = (
+ [ PREREQ_IGNORE, # conf value
+ loc('No, do not install prerequisites'), # UI Value
+ loc("I won't install prerequisites") # diag message
+ ],
+ [ PREREQ_INSTALL,
+ loc('Yes, please install prerequisites'),
+ loc("I will install prerequisites")
+ ],
+ [ PREREQ_ASK,
+ loc('Ask me before installing a prerequisite'),
+ loc("I will ask permission to install")
+ ],
+ [ PREREQ_BUILD,
+ loc('Build prerequisites, but do not install them'),
+ loc( "I will only build, but not install prerequisites" )
+ ],
+ );
+
+ my %reply = map { $_->[1] => $_->[0] } @map; # choice => value
+ my %diag = map { $_->[1] => $_->[2] } @map; # choice => diag message
+ my %conf = map { $_->[0] => $_->[1] } @map; # value => ui choice
+
+ my $reply = $term->get_reply(
+ prompt => loc('Follow prerequisites?'),
+ default => $conf{ $conf->get_conf( $type ) },
+ choices => [ @conf{ sort keys %conf } ],
+ );
+ print "\n";
+
+ my $value = $reply{ $reply };
+ my $diag = $diag{ $reply };
+
+ $conf->set_conf( $type => $value );
+ print $diag, "\n";
+ }
+
+ { print loc("
+Modules in the CPAN archives are protected with md5 checksums.
+
+This requires the Perl module Digest::MD5 to be installed (which
+CPANPLUS can do for you later);
+
+ ");
+ my $type = 'md5';
+
+ my $yn = $term->ask_yn(
+ prompt => loc("Shall I use the MD5 checksums?"),
+ default => $conf->get_conf( $type ),
+ );
+
+ print $yn
+ ? loc("I will use the MD5 checksums if you have it")
+ : loc("I won't use the MD5 checksums");
+
+ $conf->set_conf( $type => $yn );
+
+ }
+
+
+ { ###########################################
+ ## sally sells seashells by the seashore ##
+ ###########################################
+
+ print loc("
+By default CPANPLUS uses its own shell when invoked. If you would prefer
+a different shell, such as one you have written or otherwise acquired,
+please enter the full name for your shell module.
+
+ ");
+
+ my $type = 'shell';
+ my $other = 'Other';
+ my @choices = (qw| CPANPLUS::Shell::Default
+ CPANPLUS::Shell::Classic |,
+ $other );
+ my $default = $conf->get_conf($type);
+
+ unshift @choices, $default unless grep { $_ eq $default } @choices;
+
+ my $reply = $term->get_reply(
+ prompt => loc('Which CPANPLUS shell do you want to use?'),
+ default => $default,
+ choices => \@choices,
+ );
+
+ if( $reply eq $other ) {
+ SHELL: {
+ $reply = $term->get_reply(
+ prompt => loc( 'Please enter the name of the shell '.
+ 'you wish to use: '),
+ );
+
+ unless( check_install( module => $reply ) ) {
+ print "\n",
+ loc("Could not find '$reply' in your path " .
+ "-- please try again"),
+ "\n";
+ redo SHELL;
+ }
+ }
+ }
+
+ print "\n", loc("Your shell is now: %1", $reply), "\n\n";
+
+ $conf->set_conf( $type => $reply );
+ }
+
+ {
+ ###################
+ ## use storable? ##
+ ###################
+
+ print loc("
+To speed up the start time of CPANPLUS, and maintain a cache over
+multiple runs, we can use Storable to freeze some information.
+Would you like to do this?
+
+");
+ my $type = 'storable';
+ my $yn = $term->ask_yn(
+ prompt => loc("Use Storable?"),
+ default => $conf->get_conf( $type ) ? 1 : 0,
+ );
+ print "\n";
+ print $yn
+ ? loc("I will use Storable if you have it")
+ : loc("I will not use Storable");
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ {
+ ###################
+ ## use cpantest? ##
+ ###################
+
+ print loc("
+CPANPLUS has support for the Test::Reporter module, which can be utilized
+to report success and failures of modules installed by CPANPLUS. Would
+you like to do this? Note that you will still be prompted before
+sending each report.
+
+If you don't have all the required modules installed yet, you should
+consider installing '%1'
+
+This package bundles all the required modules to enable test reporting
+and querying from CPANPLUS.
+You can do so straight after this installation.
+
+ ", 'Bundle::CPANPLUS::Test::Reporter');
+
+ my $type = 'cpantest';
+ my $yn = $term->ask_yn(
+ prompt => loc('Report test results?'),
+ default => $conf->get_conf( $type ) ? 1 : 0,
+ );
+
+ print "\n";
+ print $yn
+ ? loc("I will prompt you to report test results")
+ : loc("I won't prompt you to report test results");
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ {
+ ###################################
+ ## use cryptographic signatures? ##
+ ###################################
+
+ print loc("
+The Module::Signature extension allows CPAN authors to sign their
+distributions using PGP signatures. Would you like to check for
+module's cryptographic integrity before attempting to install them?
+Note that this requires either the 'gpg' utility or Crypt::OpenPGP
+to be installed.
+
+ ");
+ my $type = 'signature';
+
+ my $yn = $term->ask_yn(
+ prompt => loc('Shall I check module signatures?'),
+ default => $conf->get_conf($type) ? 1 : 0,
+ );
+
+ print "\n";
+ print $yn
+ ? loc("Ok, I will attempt to check module signatures.")
+ : loc("Ok, I won't attempt to check module signatures.");
+
+ $conf->set_conf( $type => $yn );
+ }
+
+ return 1;
+}
+
+sub _setup_hosts {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->configure_object;
+
+
+ if( scalar @{ $conf->get_conf('hosts') } ) {
+
+ my $hosts;
+ for my $href ( @{$conf->get_conf('hosts')} ) {
+ $hosts .= "\t$href->{scheme}://$href->{host}$href->{path}\n";
+ }
+
+ print loc("
+I see you already have some hosts selected:
+
+$hosts
+
+If you'd like to stick with your current settings, just select 'Yes'.
+Otherwise, select 'No' and you can reconfigure your hosts
+
+");
+ my $yn = $term->ask_yn(
+ prompt => loc("Would you like to keep your current hosts?"),
+ default => 'y',
+ );
+ return 1 if $yn;
+ }
+
+ my @hosts;
+ MAIN: {
+
+ print loc("
+Now we need to know where your favorite CPAN sites are located. Make a
+list of a few sites (just in case the first on the array won't work).
+
+If you are mirroring CPAN to your local workstation, specify a file:
+URI by picking the CUSTOM option.
+
+Otherwise, let us fetch the official CPAN mirror list and you can pick
+the mirror that suits you best from a list by using the MIRROR option;
+First, pick a nearby continent and country. Then, you will be presented
+with a list of URLs of CPAN mirrors in the country you selected. Select
+one or more of those URLs.
+
+Note, the latter option requires a working net connection.
+
+You can select VIEW to see your current selection and QUIT when you
+are done.
+
+");
+
+ my $reply = $term->get_reply(
+ prompt => loc('Please choose an option'),
+ choices => [qw|Mirror Custom View Quit|],
+ default => 'Mirror',
+ );
+
+ goto MIRROR if $reply eq 'Mirror';
+ goto CUSTOM if $reply eq 'Custom';
+ goto QUIT if $reply eq 'Quit';
+
+ $self->_view_hosts(@hosts) if $reply eq 'View';
+ redo MAIN;
+ }
+
+ my $mirror_file;
+ my $hosts;
+ MIRROR: {
+ $mirror_file ||= $self->_get_mirrored_by or return;
+ $hosts ||= $self->_parse_mirrored_by($mirror_file) or return;
+
+ my ($continent, $country, $host) = $self->_guess_from_timezone( $hosts );
+
+ CONTINENT: {
+ my %seen;
+ my @choices = sort map {
+ $_->{'continent'}
+ } grep {
+ not $seen{$_->{'continent'}}++
+ } values %$hosts;
+ push @choices, qw[Custom Up Quit];
+
+ my $reply = $term->get_reply(
+ prompt => loc('Pick a continent'),
+ default => $continent,
+ choices => \@choices,
+ );
+
+ goto MAIN if $reply eq 'Up';
+ goto CUSTOM if $reply eq 'Custom';
+ goto QUIT if $reply eq 'Quit';
+
+ $continent = $reply;
+ }
+
+ COUNTRY: {
+ my %seen;
+ my @choices = sort map {
+ $_->{'country'}
+ } grep {
+ not $seen{$_->{'country'}}++
+ } grep {
+ ($_->{'continent'} eq $continent)
+ } values %$hosts;
+ push @choices, qw[Custom Up Quit];
+
+ my $reply = $term->get_reply(
+ prompt => loc('Pick a country'),
+ default => $country,
+ choices => \@choices,
+ );
+
+ goto CONTINENT if $reply eq 'Up';
+ goto CUSTOM if $reply eq 'Custom';
+ goto QUIT if $reply eq 'Quit';
+
+ $country = $reply;
+ }
+
+ HOST: {
+ my @list = grep {
+ $_->{'continent'} eq $continent and
+ $_->{'country'} eq $country
+ } values %$hosts;
+
+ my %map; my $default;
+ for my $href (@list) {
+ for my $con ( @{$href->{'connections'}} ) {
+ next unless length $con->{'host'};
+
+ my $entry = $con->{'scheme'} . '://' . $con->{'host'};
+ $default = $entry if $con->{'host'} eq $host;
+
+ $map{$entry} = $con;
+ }
+ }
+
+ CHOICE: {
+
+ ### doesn't play nice with Term::UI :(
+ ### should make t::ui figure out pager opens
+ #$self->_pager_open; # host lists might be long
+
+ print loc("
+You can enter multiple sites by seperating them by a space.
+For example:
+ 1 4 2 5
+ ");
+
+ my @reply = $term->get_reply(
+ prompt => loc('Please pick a site: '),
+ choices => [sort(keys %map),
+ qw|Custom View Up Quit|],
+ default => $default,
+ multi => 1,
+ );
+ #$self->_pager_close;
+
+
+ goto COUNTRY if grep { $_ eq 'Up' } @reply;
+ goto CUSTOM if grep { $_ eq 'Custom' } @reply;
+ goto QUIT if grep { $_ eq 'Quit' } @reply;
+
+ ### add the host, but only if it's not on the stack already ###
+ unless( grep { $_ eq 'View' } @reply ) {
+ for my $reply (@reply) {
+ if( grep { $_ eq $map{$reply} } @hosts ) {
+ print loc("Host '%1' already selected", $reply);
+ print "\n\n";
+ } else {
+ push @hosts, $map{$reply}
+ }
+ }
+ }
+
+ $self->_view_hosts(@hosts);
+
+ goto QUIT if $self->autoreply;
+ redo CHOICE;
+ }
+ }
+ }
+
+ CUSTOM: {
+ print loc("
+If there are any additional URLs you would like to use, please add them
+now. You may enter them separately or as a space delimited list.
+
+We provide a default fall-back URL, but you are welcome to override it
+with e.g. 'http://www.cpan.org/' if LWP, wget or curl is installed.
+
+(Enter a single space when you are done, or to simply skip this step.)
+
+Note that if you want to use a local depository, you will have to enter
+as follows:
+
+file://server/path/to/cpan
+
+if the file is on a server on your local network or as:
+
+file:///path/to/cpan
+
+if the file is on your local disk. Note the three /// after the file: bit
+
+");
+
+ CHOICE: {
+ my $reply = $term->get_reply(
+ prompt => loc("Additionals host(s) to add: "),
+ default => '',
+ );
+
+ last CHOICE unless $reply =~ /\S/;
+
+ my $href = $self->_parse_host($reply);
+
+ if( $href ) {
+ push @hosts, $href
+ unless grep {
+ $href->{'scheme'} eq $_->{'scheme'} and
+ $href->{'host'} eq $_->{'host'} and
+ $href->{'path'} eq $_->{'path'}
+ } @hosts;
+
+ last CHOICE if $self->autoreply;
+ } else {
+ print loc("Invalid uri! Please try again!");
+ }
+
+ $self->_view_hosts(@hosts);
+
+ redo CHOICE;
+ }
+
+ DONE: {
+
+ print loc("
+Where would you like to go now?
+
+Please pick one of the following options or Quit when you are done
+
+");
+ my $answer = $term->get_reply(
+ prompt => loc("Where to now?"),
+ default => 'Quit',
+ choices => [qw|Mirror Custom View Quit|],
+ );
+
+ if( $answer eq 'View' ) {
+ $self->_view_hosts(@hosts);
+ redo DONE;
+ }
+
+ goto MIRROR if $answer eq 'Mirror';
+ goto CUSTOM if $answer eq 'Custom';
+ goto QUIT if $answer eq 'Quit';
+ }
+ }
+
+ QUIT: {
+ $conf->set_conf( hosts => \@hosts );
+
+ print loc("
+Your host configuration has been saved
+
+");
+ }
+
+ return 1;
+}
+
+sub _view_hosts {
+ my $self = shift;
+ my @hosts = @_;
+
+ print "\n\n";
+
+ if( scalar @hosts ) {
+ my $i = 1;
+ for my $host (@hosts) {
+
+ ### show full path on file uris, otherwise, just show host
+ my $path = join '', (
+ $host->{'scheme'} eq 'file'
+ ? ( ($host->{'host'} || '[localhost]'),
+ $host->{path} )
+ : $host->{'host'}
+ );
+
+ printf "%-40s %30s\n",
+ loc("Selected %1",$host->{'scheme'} . '://' . $path ),
+ loc("%quant(%2,host) selected thus far.", $i);
+ $i++;
+ }
+ } else {
+ print loc("No hosts selected so far.");
+ }
+
+ print "\n\n";
+
+ return 1;
+}
+
+sub _get_mirrored_by {
+ my $self = shift;
+ my $cpan = $self->backend;
+ my $conf = $self->configure_object;
+
+ print loc("
+Now, we are going to fetch the mirror list for first-time configurations.
+This may take a while...
+
+");
+
+ ### use the enew configuratoin ###
+ $cpan->configure_object( $conf );
+
+ load CPANPLUS::Module::Fake;
+ load CPANPLUS::Module::Author::Fake;
+
+ my $mb = CPANPLUS::Module::Fake->new(
+ module => $conf->_get_source('hosts'),
+ path => '',
+ package => $conf->_get_source('hosts'),
+ author => CPANPLUS::Module::Author::Fake->new(
+ _id => $cpan->_id ),
+ _id => $cpan->_id,
+ );
+
+ my $file = $cpan->_fetch( fetchdir => $conf->get_conf('base'),
+ module => $mb );
+
+ return $file if $file;
+ return;
+}
+
+sub _parse_mirrored_by {
+ my $self = shift;
+ my $file = shift;
+
+ -s $file or return;
+
+ my $fh = new FileHandle;
+ $fh->open("$file")
+ or (
+ warn(loc('Could not open file "%1": %2', $file, $!)),
+ return
+ );
+
+ ### slurp the file in ###
+ { local $/; $file = <$fh> }
+
+ ### remove comments ###
+ $file =~ s/#.*$//gm;
+
+ $fh->close;
+
+ ### sample host entry ###
+ # ftp.sun.ac.za:
+ # frequency = "daily"
+ # dst_ftp = "ftp://ftp.sun.ac.za/CPAN/CPAN/"
+ # dst_location = "Stellenbosch, South Africa, Africa (-26.1992 28.0564)"
+ # dst_organisation = "University of Stellenbosch"
+ # dst_timezone = "+2"
+ # dst_contact = "ftpadm@ftp.sun.ac.za"
+ # dst_src = "ftp.funet.fi"
+ #
+ # # dst_dst = "ftp://ftp.sun.ac.za/CPAN/CPAN/"
+ # # dst_contact = "mailto:ftpadm@ftp.sun.ac.za
+ # # dst_src = "ftp.funet.fi"
+
+ ### host name as key, rest of the entry as value ###
+ my %hosts = $file =~ m/([a-zA-Z0-9\-\.]+):\s+((?:\w+\s+=\s+".*?"\s+)+)/gs;
+
+ while (my($host,$data) = each %hosts) {
+
+ my $href;
+ map {
+ s/^\s*//;
+ my @a = split /\s*=\s*/;
+ $a[1] =~ s/^"(.+?)"$/$1/g;
+ $href->{ pop @a } = pop @a;
+ } grep /\S/, split /\n/, $data;
+
+ ($href->{city_area}, $href->{country}, $href->{continent},
+ $href->{latitude}, $href->{longitude} ) =
+ $href->{dst_location} =~
+ m/
+ #Aizu-Wakamatsu, Tohoku-chiho, Fukushima
+ ^"?(
+ (?:[^,]+?)\s* # city
+ (?:
+ (?:,\s*[^,]+?)\s* # optional area
+ )*? # some have multiple areas listed
+ )
+
+ #Japan
+ ,\s*([^,]+?)\s* # country
+
+ #Asia
+ ,\s*([^,]+?)\s* # continent
+
+ # (37.4333 139.9821)
+ \((\S+)\s+(\S+?)\)"?$ # (latitude longitude)
+ /sx;
+
+ ### parse the different hosts, store them in config format ###
+ my @list;
+
+ for my $type (qw[dst_ftp dst_rsync dst_http]) {
+ my $path = $href->{$type};
+ next unless $path =~ /\w/;
+ if ($type eq 'dst_rsync' && $path !~ /^rsync:/) {
+ $path =~ s{::}{/};
+ $path = "rsync://$path/";
+ }
+ my $parts = $self->_parse_host($path);
+ push @list, $parts;
+ }
+
+ $href->{connections} = \@list;
+ $hosts{$host} = $href;
+ }
+
+ return \%hosts;
+}
+
+sub _parse_host {
+ my $self = shift;
+ my $host = shift;
+
+ my @parts = $host =~ m|^(\w*)://([^/]*)(/.*)$|s;
+
+ my $href;
+ for my $key (qw[scheme host path]) {
+ $href->{$key} = shift @parts;
+ }
+
+ return if lc($href->{'scheme'}) ne 'file' and !$href->{'host'};
+ return if !$href->{'path'};
+
+ return $href;
+}
+
+## tries to figure out close hosts based on your timezone
+##
+## Currently can only report on unique items for each of zones, countries, and
+## sites. In the future this will be combined with something else (perhaps a
+## ping?) to narrow down multiple choices.
+##
+## Tries to return the best zone, country, and site for your location. Any non-
+## unique items will be set to undef instead.
+##
+## (takes hashref, returns array)
+##
+sub _guess_from_timezone {
+ my $self = shift;
+ my $hosts = shift;
+ my (%zones, %countries, %sites);
+
+ ### autrijus - build time zone table
+ my %freq_weight = (
+ 'hourly' => 2400,
+ '4 times a day' => 400,
+ '4x daily' => 400,
+ 'daily' => 100,
+ 'twice daily' => 50,
+ 'weekly' => 15,
+ );
+
+ while (my ($site, $host) = each %{$hosts}) {
+ my ($zone, $continent, $country, $frequency) =
+ @{$host}{qw/dst_timezone continent country frequency/};
+
+
+ # skip non-well-formed ones
+ next unless $continent and $country and $zone =~ /^[-+]?\d+(?::30)?/;
+ ### fix style
+ chomp $zone;
+ $zone =~ s/:30/.5/;
+ $zone =~ s/^\+//;
+ $zone =~ s/"//g;
+
+ $zones{$zone}{$continent}++;
+ $countries{$zone}{$continent}{$country}++;
+ $sites{$zone}{$continent}{$country}{$site} = $freq_weight{$frequency};
+ }
+
+ use Time::Local;
+ my $offset = ((timegm(localtime) - timegm(gmtime)) / 3600);
+
+ local $_;
+
+ ## pick the entry with most country/site/frequency, one level each;
+ ## note it has to be sorted -- otherwise we're depending on the hash order.
+ ## also, the list context assignment (pick first one) is deliberate.
+
+ my ($continent) = map {
+ (sort { ($_->{$b} <=> $_->{$a}) or $b cmp $a } keys(%{$_}))
+ } $zones{$offset};
+
+ my ($country) = map {
+ (sort { ($_->{$b} <=> $_->{$a}) or $b cmp $a } keys(%{$_}))
+ } $countries{$offset}{$continent};
+
+ my ($site) = map {
+ (sort { ($_->{$b} <=> $_->{$a}) or $b cmp $a } keys(%{$_}))
+ } $sites{$offset}{$continent}{$country};
+
+ return ($continent, $country, $site);
+} # _guess_from_timezone
+
+
+### big big regex, stolen to check if you enter a valid address
+{
+ my $RFC822PAT; # RFC pattern to match for valid email address
+
+ sub _valid_email {
+ my $self = shift;
+ if (!$RFC822PAT) {
+ my $esc = '\\\\'; my $Period = '\.'; my $space = '\040';
+ my $tab = '\t'; my $OpenBR = '\['; my $CloseBR = '\]';
+ my $OpenParen = '\('; my $CloseParen = '\)'; my $NonASCII = '\x80-\xff';
+ my $ctrl = '\000-\037'; my $CRlist = '\012\015';
+
+ my $qtext = qq/[^$esc$NonASCII$CRlist\"]/;
+ my $dtext = qq/[^$esc$NonASCII$CRlist$OpenBR$CloseBR]/;
+ my $quoted_pair = qq< $esc [^$NonASCII] >; # an escaped character
+ my $ctext = qq< [^$esc$NonASCII$CRlist()] >;
+ my $Cnested = qq< $OpenParen $ctext* (?: $quoted_pair $ctext* )* $CloseParen >;
+ my $comment = qq< $OpenParen $ctext* (?: (?: $quoted_pair | $Cnested ) $ctext* )* $CloseParen >;
+ my $X = qq< [$space$tab]* (?: $comment [$space$tab]* )* >;
+ my $atom_char = qq/[^($space)<>\@,;:\".$esc$OpenBR$CloseBR$ctrl$NonASCII]/;
+ my $atom = qq< $atom_char+ (?!$atom_char) >;
+ my $quoted_str = qq< \" $qtext * (?: $quoted_pair $qtext * )* \" >;
+ my $word = qq< (?: $atom | $quoted_str ) >;
+ my $domain_ref = $atom;
+ my $domain_lit = qq< $OpenBR (?: $dtext | $quoted_pair )* $CloseBR >;
+ my $sub_domain = qq< (?: $domain_ref | $domain_lit) $X >;
+ my $domain = qq< $sub_domain (?: $Period $X $sub_domain)* >;
+ my $route = qq< \@ $X $domain (?: , $X \@ $X $domain )* : $X >;
+ my $local_part = qq< $word $X (?: $Period $X $word $X )* >;
+ my $addr_spec = qq< $local_part \@ $X $domain >;
+ my $route_addr = qq[ < $X (?: $route )? $addr_spec > ];
+ my $phrase_ctrl = '\000-\010\012-\037'; # like ctrl, but without tab
+ my $phrase_char = qq/[^()<>\@,;:\".$esc$OpenBR$CloseBR$NonASCII$phrase_ctrl]/;
+ my $phrase = qq< $word $phrase_char * (?: (?: $comment | $quoted_str ) $phrase_char * )* >;
+ $RFC822PAT = qq< $X (?: $addr_spec | $phrase $route_addr) >;
+ }
+
+ return scalar ($_[0] =~ /$RFC822PAT/ox);
+ }
+}
+
+
+
+
+
+
+1;
+
+
+sub _edit {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my $file = shift || $conf->_config_pm_to_file( $self->config_type );
+ my $editor = shift || $conf->get_program('editor');
+ my $term = $self->term;
+
+ unless( $editor ) {
+ print loc("
+I'm sorry, I can't find a suitable editor, so I can't offer you
+post-configuration editing of the config file
+
+");
+ return 1;
+ }
+
+ ### save the thing first, so there's something to edit
+ $self->_save;
+
+ return !system("$editor $file");
+}
+
+sub _save {
+ my $self = shift;
+ my $conf = $self->configure_object;
+
+ return $conf->save( $self->config_type );
+}
+
+1;
diff --git a/lib/CPANPLUS/Dist.pm b/lib/CPANPLUS/Dist.pm
new file mode 100644
index 0000000000..50acb48bc4
--- /dev/null
+++ b/lib/CPANPLUS/Dist.pm
@@ -0,0 +1,505 @@
+package CPANPLUS::Dist;
+
+use strict;
+
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load check_install];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+use Object::Accessor;
+
+local $Params::Check::VERBOSE = 1;
+
+my @methods = qw[status parent];
+for my $key ( @methods ) {
+ no strict 'refs';
+ *{__PACKAGE__."::$key"} = sub {
+ my $self = shift;
+ $self->{$key} = $_[0] if @_;
+ return $self->{$key};
+ }
+}
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Dist
+
+=head1 SYNOPSIS
+
+ my $dist = CPANPLUS::Dist->new(
+ format => 'build',
+ module => $modobj,
+ );
+
+=head1 DESCRIPTION
+
+C<CPANPLUS::Dist> is a base class for any type of C<CPANPLUS::Dist::>
+modules.
+
+=head1 ACCESSORS
+
+=over 4
+
+=item parent()
+
+Returns the C<CPANPLUS::Module> object that parented this object.
+
+=item status()
+
+Returns the C<Object::Accessor> object that keeps the status for
+this module.
+
+=back
+
+=head1 STATUS ACCESSORS
+
+All accessors can be accessed as follows:
+ $deb->status->ACCESSOR
+
+=over 4
+
+=item created()
+
+Boolean indicating whether the dist was created successfully.
+Explicitly set to C<0> when failed, so a value of C<undef> may be
+interpreted as C<not yet attempted>.
+
+=item installed()
+
+Boolean indicating whether the dist was installed successfully.
+Explicitly set to C<0> when failed, so a value of C<undef> may be
+interpreted as C<not yet attempted>.
+
+=item uninstalled()
+
+Boolean indicating whether the dist was uninstalled successfully.
+Explicitly set to C<0> when failed, so a value of C<undef> may be
+interpreted as C<not yet attempted>.
+
+=item dist()
+
+The location of the final distribution. This may be a file or
+directory, depending on how your distribution plug in of choice
+works. This will be set upon a successful create.
+
+=cut
+
+=head2 $dist = CPANPLUS::Dist->new( module => MODOBJ, [format => DIST_TYPE] );
+
+Create a new C<CPANPLUS::Dist> object based on the provided C<MODOBJ>.
+The optional argument C<format> is used to indicate what type of dist
+you would like to create (like C<makemaker> for a C<CPANPLUS::Dist::MM>
+object, C<build> for a C<CPANPLUS::Dist::Build> object, and so on ).
+If not provided, will default to the setting as specified by your
+config C<dist_type>.
+
+Returns a C<CPANPLUS::Dist> object on success and false on failure.
+
+=cut
+
+sub new {
+ my $self = shift;
+ my %hash = @_;
+
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ ### first verify we got a module object ###
+ my $mod;
+ my $tmpl = {
+ module => { required => 1, allow => IS_MODOBJ, store => \$mod },
+ };
+ check( $tmpl, \%hash ) or return;
+
+ ### get the conf object ###
+ my $conf = $mod->parent->configure_object();
+
+ ### figure out what type of dist object to create ###
+ my $format;
+ my $tmpl2 = {
+ format => { default => $conf->get_conf('dist_type'),
+ allow => [ __PACKAGE__->dist_types ],
+ store => \$format },
+ };
+ check( $tmpl2, \%hash ) or return;
+
+
+ unless( can_load( modules => { $format => '0.0' }, verbose => 1 ) ) {
+ error(loc("'%1' not found -- you need '%2' version '%3' or higher ".
+ "to detect plugins", $format, 'Module::Pluggable','2.4'));
+ return;
+ }
+
+ ### bless the object in the child class ###
+ my $obj = bless { parent => $mod }, $format;
+
+ ### check if the format is available in this environment ###
+ if( $conf->_get_build('sanity_check') and not $obj->format_available ) {
+ error( loc( "Format '%1' is not available",$format) );
+ return;
+ }
+
+ ### create a status object ###
+ { my $acc = Object::Accessor->new;
+ $obj->status($acc);
+
+ ### add minimum supported accessors
+ $acc->mk_accessors( qw[prepared created installed uninstalled
+ distdir dist] );
+ }
+
+ ### now initialize it or admit failure
+ unless( $obj->init ) {
+ error(loc("Dist initialization of '%1' failed for '%2'",
+ $format, $mod->module));
+ return;
+ }
+
+ ### return the object
+ return $obj;
+}
+
+=head2 @dists = CPANPLUS::Dist->dist_types;
+
+Returns a list of the CPANPLUS::Dist::* classes available
+
+=cut
+
+### returns a list of dist_types we support
+### will get overridden by Module::Pluggable if loaded
+### XXX add support for 'plugin' dir in config as well
+{ my $Loaded;
+ my @Dists = (INSTALLER_MM);
+ my @Ignore = ();
+
+ ### backdoor method to add more dist types
+ sub _add_dist_types { my $self = shift; push @Dists, @_ };
+
+ ### backdoor method to exclude dist types
+ sub _ignore_dist_types { my $self = shift; push @Ignore, @_ };
+
+ ### locally add the plugins dir to @INC, so we can find extra plugins
+ #local @INC = @INC, File::Spec->catdir(
+ # $conf->get_conf('base'),
+ # $conf->_get_build('plugins') );
+
+ ### load any possible plugins
+ sub dist_types {
+
+ if ( !$Loaded++ and check_install( module => 'Module::Pluggable',
+ version => '2.4')
+ ) {
+ require Module::Pluggable;
+
+ my $only_re = __PACKAGE__ . '::\w+$';
+
+ Module::Pluggable->import(
+ sub_name => '_dist_types',
+ search_path => __PACKAGE__,
+ only => qr/$only_re/,
+ except => [ INSTALLER_MM,
+ INSTALLER_SAMPLE,
+ INSTALLER_BASE,
+ ]
+ );
+ my %ignore = map { $_ => $_ } @Ignore;
+
+ push @Dists, grep { not $ignore{$_} } __PACKAGE__->_dist_types;
+ }
+
+ return @Dists;
+ }
+}
+
+=head2 prereq_satisfied( modobj => $modobj, version => $version_spec )
+
+Returns true if this prereq is satisfied. Returns false if it's not.
+Also issues an error if it seems "unsatisfiable," i.e. if it can't be
+found on CPAN or the latest CPAN version doesn't satisfy it.
+
+=cut
+
+sub prereq_satisfied {
+ my $dist = shift;
+ my $cb = $dist->parent->parent;
+ my %hash = @_;
+
+ my($mod,$ver);
+ my $tmpl = {
+ version => { required => 1, store => \$ver },
+ modobj => { required => 1, store => \$mod, allow => IS_MODOBJ },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ return 1 if $mod->is_uptodate( version => $ver );
+
+ if ( $cb->_vcmp( $ver, $mod->version ) > 0 ) {
+
+ error(loc(
+ "This distribution depends on %1, but the latest version".
+ " of %2 on CPAN (%3) doesn't satisfy the specific version".
+ " dependency (%4). You may have to resolve this dependency ".
+ "manually.",
+ $mod->module, $mod->module, $mod->version, $ver ));
+
+ }
+
+ return;
+}
+
+=head2 _resolve_prereqs
+
+Makes sure prerequisites are resolved
+
+XXX Need docs, internal use only
+
+=cut
+
+sub _resolve_prereqs {
+ my $dist = shift;
+ my $self = $dist->parent;
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my ($prereqs, $format, $verbose, $target, $force, $prereq_build);
+ my $tmpl = {
+ ### XXX perhaps this should not be required, since it may not be
+ ### packaged, just installed...
+ ### Let it be empty as well -- that means the $modobj->install
+ ### routine will figure it out, which is fine if we didn't have any
+ ### very specific wishes (it will even detect the favourite
+ ### dist_type).
+ format => { required => 1, store => \$format,
+ allow => ['',__PACKAGE__->dist_types], },
+ prereqs => { required => 1, default => { },
+ strict_type => 1, store => \$prereqs },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ ### make sure allow matches with $mod->install's list
+ target => { default => '', store => \$target,
+ allow => ['',qw[create ignore install]] },
+ prereq_build => { default => 0, store => \$prereq_build },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### so there are no prereqs? then don't even bother
+ return 1 unless keys %$prereqs;
+
+ ### so you didn't provide an explicit target.
+ ### maybe your config can tell us what to do.
+ $target ||= {
+ PREREQ_ASK, TARGET_INSTALL, # we'll bail out if the user says no
+ PREREQ_BUILD, TARGET_CREATE,
+ PREREQ_IGNORE, TARGET_IGNORE,
+ PREREQ_INSTALL, TARGET_INSTALL,
+ }->{ $conf->get_conf('prereqs') } || '';
+
+ ### XXX BIG NASTY HACK XXX FIXME at some point.
+ ### when installing Bundle::CPANPLUS::Dependencies, we want to
+ ### install all packages matching 'cpanplus' to be installed last,
+ ### as all CPANPLUS' prereqs are being installed as well, but are
+ ### being loaded for bootstrapping purposes. This means CPANPLUS
+ ### can find them, but for example cpanplus::dist::build won't,
+ ### which gets messy FAST. So, here we sort our prereqs only IF
+ ### the parent module is Bundle::CPANPLUS::Dependencies.
+ ### Really, we would wnat some sort of sorted prereq mechanism,
+ ### but Bundle:: doesn't support it, and we flatten everything
+ ### to a hash internally. A sorted hash *might* do the trick if
+ ### we got a transparent implementation.. that would mean we would
+ ### just have to remove the 'sort' here, and all will be well
+ my @sorted_prereqs;
+
+ ### use regex, could either be a module name, or a package name
+ if( $self->module =~ /^Bundle(::|-)CPANPLUS(::|-)Dependencies/ ) {
+ my (@first, @last);
+ for my $mod ( sort keys %$prereqs ) {
+ $mod =~ /CPANPLUS/
+ ? push @last, $mod
+ : push @first, $mod;
+ }
+ @sorted_prereqs = (@first, @last);
+ } else {
+ @sorted_prereqs = sort keys %$prereqs;
+ }
+
+ ### first, transfer this key/value pairing into a
+ ### list of module objects + desired versions
+ my @install_me;
+
+ for my $mod ( @sorted_prereqs ) {
+ my $version = $prereqs->{$mod};
+ my $modobj = $cb->module_tree($mod);
+
+ #### XXX we ignore the version, and just assume that the latest
+ #### version from cpan will meet your requirements... dodgy =/
+ unless( $modobj ) {
+ error( loc( "No such module '%1' found on CPAN", $mod ) );
+ next;
+ }
+
+ ### it's not uptodate, we need to install it
+ if( !$dist->prereq_satisfied(modobj => $modobj, version => $version)) {
+ msg(loc("Module '%1' requires '%2' version '%3' to be installed ",
+ $self->module, $modobj->module, $version), $verbose );
+
+ push @install_me, [$modobj, $version];
+
+ ### it's not an MM or Build format, that means it's a package
+ ### manager... we'll need to install it as well, via the PM
+ } elsif ( INSTALL_VIA_PACKAGE_MANAGER->($format) and
+ !$modobj->package_is_perl_core and
+ ($target ne TARGET_IGNORE)
+ ) {
+ msg(loc("Module '%1' depends on '%2', may need to build a '%3' ".
+ "package for it as well", $self->module, $modobj->module,
+ $format));
+ push @install_me, [$modobj, $version];
+ }
+ }
+
+
+
+ ### so you just want to ignore prereqs? ###
+ if( $target eq TARGET_IGNORE ) {
+
+ ### but you have modules you need to install
+ if( @install_me ) {
+ msg(loc("Ignoring prereqs, this may mean your install will fail"),
+ $verbose);
+ msg(loc("'%1' listed the following dependencies:", $self->module),
+ $verbose);
+
+ for my $aref (@install_me) {
+ my ($mod,$version) = @$aref;
+
+ my $str = sprintf "\t%-35s %8s\n", $mod->module, $version;
+ msg($str,$verbose);
+ }
+
+ return;
+
+ ### ok, no problem, you have all needed prereqs anyway
+ } else {
+ return 1;
+ }
+ }
+
+ my $flag;
+ for my $aref (@install_me) {
+ my($modobj,$version) = @$aref;
+
+ ### another prereq may have already installed this one...
+ ### so dont ask again if the module turns out to be uptodate
+ ### see bug [#11840]
+ ### if either force or prereq_build are given, the prereq
+ ### should be built anyway
+ next if (!$force and !$prereq_build) &&
+ $dist->prereq_satisfied(modobj => $modobj, version => $version);
+
+ ### either we're told to ignore the prereq,
+ ### or the user wants us to ask him
+ if( ( $conf->get_conf('prereqs') == PREREQ_ASK and not
+ $cb->_callbacks->install_prerequisite->($self, $modobj)
+ )
+ ) {
+ msg(loc("Will not install prerequisite '%1' -- Note " .
+ "that the overall install may fail due to this",
+ $modobj->module), $verbose);
+ next;
+ }
+
+ ### value set and false -- means failure ###
+ if( defined $modobj->status->installed
+ && !$modobj->status->installed
+ ) {
+ error( loc( "Prerequisite '%1' failed to install before in " .
+ "this session", $modobj->module ) );
+ $flag++;
+ last;
+ }
+
+ ### part of core?
+ if( $modobj->package_is_perl_core ) {
+ error(loc("Prerequisite '%1' is perl-core (%2) -- not ".
+ "installing that. Aborting install",
+ $modobj->module, $modobj->package ) );
+ $flag++;
+ last;
+ }
+
+ ### circular dependency code ###
+ my $pending = $cb->_status->pending_prereqs || {};
+
+ ### recursive dependency ###
+ if ( $pending->{ $modobj->module } ) {
+ error( loc( "Recursive dependency detected (%1) -- skipping",
+ $modobj->module ) );
+ next;
+ }
+
+ ### register this dependency as pending ###
+ $pending->{ $modobj->module } = $modobj;
+ $cb->_status->pending_prereqs( $pending );
+
+
+ ### call $modobj->install rather than doing
+ ### CPANPLUS::Dist->new and the like ourselves,
+ ### since ->install will take care of fetch &&
+ ### extract as well
+ my $pa = $dist->status->_prepare_args || {};
+ my $ca = $dist->status->_create_args || {};
+ my $ia = $dist->status->_install_args || {};
+
+ unless( $modobj->install( %$pa, %$ca, %$ia,
+ force => $force,
+ verbose => $verbose,
+ format => $format,
+ target => $target )
+ ) {
+ error(loc("Failed to install '%1' as prerequisite " .
+ "for '%2'", $modobj->module, $self->module ) );
+ $flag++;
+ }
+
+ ### unregister the pending dependency ###
+ $pending->{ $modobj->module } = 0;
+ $cb->_status->pending_prereqs( $pending );
+
+ last if $flag;
+
+ ### don't want us to install? ###
+ if( $target ne TARGET_INSTALL ) {
+ my $dir = $modobj->status->extract
+ or error(loc("No extraction dir for '%1' found ".
+ "-- weird", $modobj->module));
+
+ $modobj->add_to_includepath();
+
+ next;
+ }
+ }
+
+ ### reset the $prereqs iterator, in case we bailed out early ###
+ keys %$prereqs;
+
+ return 1 unless $flag;
+ return;
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Dist/Base.pm b/lib/CPANPLUS/Dist/Base.pm
new file mode 100644
index 0000000000..2ba0abb64b
--- /dev/null
+++ b/lib/CPANPLUS/Dist/Base.pm
@@ -0,0 +1,249 @@
+package CPANPLUS::Dist::Base;
+
+use strict;
+
+use vars qw[@ISA $VERSION];
+@ISA = qw[CPANPLUS::Dist];
+$VERSION = '0.01';
+
+=head1 NAME
+
+CPANPLUS::Dist::Base - Base class for custom distribution classes
+
+=head1 SYNOPSIS
+
+ package CPANPLUS::Dist::MY_IMPLEMENTATION
+
+ use base 'CPANPLUS::Dist::Base';
+
+ sub prepare {
+ my $dist = shift;
+
+ ### do the 'standard' things
+ $dist->SUPER::prepare( @_ ) or return;
+
+ ### do MY_IMPLEMENTATION specific things
+ ...
+
+ ### don't forget to set the status!
+ return $dist->status->prepared( $SUCCESS ? 1 : 0 );
+ }
+
+
+=head1 DESCRIPTION
+
+CPANPLUS::Dist::Base functions as a base class for all custom
+distribution implementations. It does all the mundane work
+CPANPLUS would have done without a custom distribution, so you
+can override just the parts you need to make your own implementation
+work.
+
+=head1 FLOW
+
+Below is a brief outline when and in which order methods in this
+class are called:
+
+ $Class->format_available; # can we use this class on this system?
+
+ $dist->init; # set up custom accessors, etc
+ $dist->prepare; # find/write meta information
+ $dist->create; # write the distribution file
+ $dist->install; # install the distribution file
+
+ $dist->uninstall; # remove the distribution (OPTIONAL)
+
+=head1 METHODS
+
+=cut
+
+
+=head2 $bool = $Class->format_available
+
+This method is called when someone requests a module to be installed
+via the superclass. This gives you the opportunity to check if all
+the needed requirements to build and install this distribution have
+been met.
+
+For example, you might need a command line program, or a certain perl
+module installed to do your job. Now is the time to check.
+
+Simply return true if the request can proceed and false if it can not.
+
+The C<CPANPLUS::Dist::Base> implementation always returns true.
+
+=cut
+
+sub format_available { return 1 }
+
+
+=head2 $bool = $dist->init
+
+This method is called just after the new dist object is set up and
+before the C<prepare> method is called. This is the time to set up
+the object so it can be used with your class.
+
+For example, you might want to add extra accessors to the C<status>
+object, which you might do as follows:
+
+ $dist->status->mk_accessors( qw[my_implementation_accessor] );
+
+The C<status> object is implemented as an instance of the
+C<Object::Accessor> class. Please refer to it's documentation for
+details.
+
+Return true if the initialization was successul, and false if it was
+not.
+
+The C<CPANPLUS::Dist::Base> implementation does not alter your object
+and always returns true.
+
+=cut
+
+sub init { return 1; }
+
+=head2 $bool = $dist->prepare
+
+This runs the preparation step of your distribution. This step is meant
+to set up the environment so the C<create> step can create the actual
+distribution(file).
+A C<prepare> call in the standard C<ExtUtils::MakeMaker> distribution
+would, for example, run C<perl Makefile.PL> to find the dependencies
+for a distribution. For a C<debian> distribution, this is where you
+would write all the metafiles required for the C<dpkg-*> tools.
+
+The C<CPANPLUS::Dist::Base> implementation simply calls the underlying
+distribution class (Typically C<CPANPLUS::Dist::MM> or
+C<CPANPLUS::Dist::Build>).
+
+Sets C<< $dist->status->prepared >> to the return value of this function.
+If you override this method, you should make sure to set this value.
+
+=cut
+
+sub prepare {
+ ### just in case you already did a create call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+ my $dist_cpan = $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+
+ $dist->status->prepared( $dist_cpan->prepare( @_ ) );
+}
+
+=head2 $bool = $dist->create
+
+This runs the creation step of your distribution. This step is meant
+to follow up on the C<prepare> call, that set up your environment so
+the C<create> step can create the actual distribution(file).
+A C<create> call in the standard C<ExtUtils::MakeMaker> distribution
+would, for example, run C<make> and C<make test> to build and test
+a distribution. For a C<debian> distribution, this is where you
+would create the actual C<.deb> file using C<dpkg>.
+
+The C<CPANPLUS::Dist::Base> implementation simply calls the underlying
+distribution class (Typically C<CPANPLUS::Dist::MM> or
+C<CPANPLUS::Dist::Build>).
+
+Sets C<< $dist->status->dist >> to the location of the created
+distribution.
+If you override this method, you should make sure to set this value.
+
+Sets C<< $dist->status->created >> to the return value of this function.
+If you override this method, you should make sure to set this value.
+
+=cut
+
+sub create {
+ ### just in case you already did a create call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+ my $dist_cpan = $self->status->dist_cpan;
+ $dist = $self->status->dist if $self->status->dist;
+ $self->status->dist( $dist ) unless $self->status->dist;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+
+ ### make sure to set this variable, if the caller hasn't yet
+ ### just so we have some clue where the dist left off.
+ $dist->status->dist( $dist_cpan->status->distdir )
+ unless defined $dist->status->dist;
+
+ $dist->status->created( $dist_cpan->create( @_ ) );
+}
+
+=head2 $bool = $dist->install
+
+This runs the install step of your distribution. This step is meant
+to follow up on the C<create> call, which prepared a distribution(file)
+to install.
+A C<create> call in the standard C<ExtUtils::MakeMaker> distribution
+would, for example, run C<make install> to copy the distribution files
+to their final destination. For a C<debian> distribution, this is where
+you would run C<dpkg --install> on the created C<.deb> file.
+
+The C<CPANPLUS::Dist::Base> implementation simply calls the underlying
+distribution class (Typically C<CPANPLUS::Dist::MM> or
+C<CPANPLUS::Dist::Build>).
+
+Sets C<< $dist->status->installed >> to the return value of this function.
+If you override this method, you should make sure to set this value.
+
+=cut
+
+sub install {
+ ### just in case you already did a create call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+ my $dist_cpan = $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+
+ $dist->status->installed( $dist_cpan->install( @_ ) );
+}
+
+=head2 $bool = $dist->uninstall
+
+This runs the uninstall step of your distribution. This step is meant
+to remove the distribution from the file system.
+A C<uninstall> call in the standard C<ExtUtils::MakeMaker> distribution
+would, for example, run C<make uninstall> to remove the distribution
+files the file system. For a C<debian> distribution, this is where you
+would run C<dpkg --uninstall PACKAGE>.
+
+The C<CPANPLUS::Dist::Base> implementation simply calls the underlying
+distribution class (Typically C<CPANPLUS::Dist::MM> or
+C<CPANPLUS::Dist::Build>).
+
+Sets C<< $dist->status->uninstalled >> to the return value of this function.
+If you override this method, you should make sure to set this value.
+
+=cut
+
+sub uninstall {
+ ### just in case you already did a create call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+ my $dist_cpan = $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+
+ $dist->status->uninstalled( $dist_cpan->uninstall( @_ ) );
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Dist/MM.pm b/lib/CPANPLUS/Dist/MM.pm
new file mode 100644
index 0000000000..f61cfc822e
--- /dev/null
+++ b/lib/CPANPLUS/Dist/MM.pm
@@ -0,0 +1,955 @@
+package CPANPLUS::Dist::MM;
+
+use strict;
+use vars qw[@ISA $STATUS];
+@ISA = qw[CPANPLUS::Dist];
+
+
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Internals::Constants::Report;
+use CPANPLUS::Error;
+use FileHandle;
+use Cwd;
+
+use IPC::Cmd qw[run];
+use Params::Check qw[check];
+use File::Basename qw[dirname];
+use Module::Load::Conditional qw[can_load check_install];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+local $Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Dist::MM
+
+=head1 SYNOPSIS
+
+ my $mm = CPANPLUS::Dist->new(
+ format => 'makemaker',
+ module => $modobj,
+ );
+ $mm->create; # runs make && make test
+ $mm->install; # runs make install
+
+
+=head1 DESCRIPTION
+
+C<CPANPLUS::Dist::MM> is a distribution class for MakeMaker related
+modules.
+Using this package, you can create, install and uninstall perl
+modules. It inherits from C<CPANPLUS::Dist>.
+
+=head1 ACCESSORS
+
+=over 4
+
+=item parent()
+
+Returns the C<CPANPLUS::Module> object that parented this object.
+
+=item status()
+
+Returns the C<Object::Accessor> object that keeps the status for
+this module.
+
+=back
+
+=head1 STATUS ACCESSORS
+
+All accessors can be accessed as follows:
+ $mm->status->ACCESSOR
+
+=over 4
+
+=item makefile ()
+
+Location of the Makefile (or Build file).
+Set to 0 explicitly if something went wrong.
+
+=item make ()
+
+BOOL indicating if the C<make> (or C<Build>) command was successful.
+
+=item test ()
+
+BOOL indicating if the C<make test> (or C<Build test>) command was
+successful.
+
+=item prepared ()
+
+BOOL indicating if the C<prepare> call exited succesfully
+This gets set after C<perl Makefile.PL>
+
+=item distdir ()
+
+Full path to the directory in which the C<prepare> call took place,
+set after a call to C<prepare>.
+
+=item created ()
+
+BOOL indicating if the C<create> call exited succesfully. This gets
+set after C<make> and C<make test>.
+
+=item installed ()
+
+BOOL indicating if the module was installed. This gets set after
+C<make install> (or C<Build install>) exits successfully.
+
+=item uninstalled ()
+
+BOOL indicating if the module was uninstalled properly.
+
+=item _create_args ()
+
+Storage of the arguments passed to C<create> for this object. Used
+for recursive calls when satisfying prerequisites.
+
+=item _install_args ()
+
+Storage of the arguments passed to C<install> for this object. Used
+for recursive calls when satisfying prerequisites.
+
+=back
+
+=cut
+
+=head1 METHODS
+
+=head2 $bool = $dist->format_available();
+
+Returns a boolean indicating whether or not you can use this package
+to create and install modules in your environment.
+
+=cut
+
+### check if the format is available ###
+sub format_available {
+ my $dist = shift;
+
+ ### we might be called as $class->format_available =/
+ require CPANPLUS::Internals;
+ my $cb = CPANPLUS::Internals->_retrieve_id(
+ CPANPLUS::Internals->_last_id );
+ my $conf = $cb->configure_object;
+
+ my $mod = "ExtUtils::MakeMaker";
+ unless( can_load( modules => { $mod => 0.0 } ) ) {
+ error( loc( "You do not have '%1' -- '%2' not available",
+ $mod, __PACKAGE__ ) );
+ return;
+ }
+
+ for my $pgm ( qw[make perlwrapper] ) {
+ unless( $conf->get_program( $pgm ) ) {
+ error(loc(
+ "You do not have '%1' in your path -- '%2' not available\n" .
+ "Please check your config entry for '%1'",
+ $pgm, __PACKAGE__ , $pgm
+ ));
+ return;
+ }
+ }
+
+ return 1;
+}
+
+=pod $bool = $dist->init();
+
+Sets up the C<CPANPLUS::Dist::MM> object for use.
+Effectively creates all the needed status accessors.
+
+Called automatically whenever you create a new C<CPANPLUS::Dist> object.
+
+=cut
+
+sub init {
+ my $dist = shift;
+ my $status = $dist->status;
+
+ $status->mk_accessors(qw[makefile make test created installed uninstalled
+ bin_make _prepare_args _create_args _install_args]
+ );
+
+ return 1;
+}
+
+=pod $bool = $dist->prepare([perl => '/path/to/perl', makemakerflags => 'EXTRA=FLAGS', force => BOOL, verbose => BOOL])
+
+C<prepare> preps a distribution for installation. This means it will
+run C<perl Makefile.PL> and determine what prerequisites this distribution
+declared.
+
+If you set C<force> to true, it will go over all the stages of the
+C<prepare> process again, ignoring any previously cached results.
+
+When running C<perl Makefile.PL>, the environment variable
+C<PERL5_CPANPLUS_IS_EXECUTING> will be set to the full path of the
+C<Makefile.PL> that is being executed. This enables any code inside
+the C<Makefile.PL> to know that it is being installed via CPANPLUS.
+
+Returns true on success and false on failure.
+
+You may then call C<< $dist->create >> on the object to create the
+installable files.
+
+=cut
+
+sub prepare {
+ ### just in case you already did a create call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+
+ ### we're also the cpan_dist, since we don't need to have anything
+ ### prepared
+ $dist = $self->status->dist_cpan if $self->status->dist_cpan;
+ $self->status->dist_cpan( $dist ) unless $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $dir;
+ unless( $dir = $self->status->extract ) {
+ error( loc( "No dir found to operate on!" ) );
+ return;
+ }
+
+$DB::single = 1;
+ my $args;
+ my( $force, $verbose, $perl, $mmflags );
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+ my $tmpl = {
+ perl => { default => $^X, store => \$perl },
+ makemakerflags => { default =>
+ $conf->get_conf('makemakerflags'),
+ store => \$mmflags },
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ ### maybe we already ran a create on this object? ###
+ return 1 if $dist->status->prepared && !$force;
+
+ ### store the arguments, so ->install can use them in recursive loops ###
+ $dist->status->_prepare_args( $args );
+
+ ### chdir to work directory ###
+ my $orig = cwd();
+ unless( $cb->_chdir( dir => $dir ) ) {
+ error( loc( "Could not chdir to build directory '%1'", $dir ) );
+ return;
+ }
+
+ my $fail;
+ RUN: {
+ ### don't run 'perl makefile.pl' again if there's a makefile already
+ if( -e MAKEFILE->() && (-M MAKEFILE->() < -M $dir) && !$force ) {
+ msg(loc("'%1' already exists, not running '%2 %3' again ".
+ " unless you force",
+ MAKEFILE->(), $perl, MAKEFILE_PL->() ), $verbose );
+
+ } else {
+ unless( -e MAKEFILE_PL->() ) {
+ msg(loc("No '%1' found - attempting to generate one",
+ MAKEFILE_PL->() ), $verbose );
+
+ $dist->write_makefile_pl(
+ verbose => $verbose,
+ force => $force
+ );
+
+ ### bail out if there's no makefile.pl ###
+ unless( -e MAKEFILE_PL->() ) {
+ error( loc( "Could not find '%1' - cannot continue",
+ MAKEFILE_PL->() ) );
+
+ ### mark that we screwed up ###
+ $dist->status->makefile(0);
+ $fail++; last RUN;
+ }
+ }
+
+ ### you can turn off running this verbose by changing
+ ### the config setting below, although it is really not
+ ### recommended
+ my $run_verbose = $verbose ||
+ $conf->get_conf('allow_build_interactivity') ||
+ 0;
+
+ ### this makes MakeMaker use defaults if possible, according
+ ### to schwern. See ticket 8047 for details.
+ local $ENV{PERL_MM_USE_DEFAULT} = 1 unless $run_verbose;
+
+ ### turn off our PERL5OPT so no modules from CPANPLUS::inc get
+ ### included in the makefile.pl -- it should build without
+ ### also, modules that run in taint mode break if we leave
+ ### our code ref in perl5opt
+ ### XXX we've removed the ENV settings from cp::inc, so only need
+ ### to reset the @INC
+ #local $ENV{PERL5OPT} = CPANPLUS::inc->original_perl5opt || '';
+
+ ### make sure it's a string, so that mmflags that have more than
+ ### one key value pair are passed as is, rather than as:
+ ### perl Makefile.PL "key=val key=>val"
+
+
+ #### XXX this needs to be the absolute path to the Makefile.PL
+ ### since cpanp-run-perl uses 'do' to execute the file, and do()
+ ### checks your @INC.. so, if there's _another_ makefile.pl in
+ ### your @INC, it will execute that one...
+ my $makefile_pl = $cb->_safe_path( path => MAKEFILE_PL->( $dir ) );
+
+ ### setting autoflush to true fixes issue from rt #8047
+ ### XXX this means that we need to keep the path to CPANPLUS
+ ### in @INC, stopping us from resolving dependencies on CPANPLUS
+ ### at bootstrap time properly.
+
+ ### XXX this fails under ipc::run due to the extra quotes,
+ ### but it works in ipc::open3. however, ipc::open3 doesn't work
+ ### on win32/cygwin. XXX TODO get a windows box and sort this out
+ # my $cmd = qq[$perl -MEnglish -le ] .
+ # QUOTE_PERL_ONE_LINER->(
+ # qq[\$OUTPUT_AUTOFLUSH++,do(q($makefile_pl))]
+ # )
+ # . $mmflags;
+
+ # my $flush = OPT_AUTOFLUSH;
+ # my $cmd = "$perl $flush $makefile_pl $mmflags";
+
+ my $run_perl = $conf->get_program('perlwrapper');
+ my $cmd = "$perl $run_perl $makefile_pl $mmflags";
+
+ ### set ENV var to tell underlying code this is what we're
+ ### executing.
+ my $captured;
+ my $rv = do {
+ my $env = ENV_CPANPLUS_IS_EXECUTING;
+ local $ENV{$env} = $makefile_pl;
+ scalar run( command => $cmd,
+ buffer => \$captured,
+ verbose => $run_verbose, # may be interactive
+ );
+ };
+
+ unless( $rv ) {
+ error( loc( "Could not run '%1 %2': %3 -- cannot continue",
+ $perl, MAKEFILE_PL->(), $captured ) );
+
+ $dist->status->makefile(0);
+ $fail++; last RUN;
+ }
+
+ ### put the output on the stack, don't print it
+ msg( $captured, 0 );
+ }
+
+ ### so, nasty feature in Module::Build, that when a Makefile.PL
+ ### is a disguised Build.PL, it generates a Build file, not a
+ ### Makefile. this breaks everything :( see rt bug #19741
+ if( not -e MAKEFILE->( $dir ) and -e BUILD_PL->( $dir ) ) {
+ error(loc(
+ "We just ran '%1' without errors, but no '%2' is ".
+ "present. However, there is a '%3' file, so this may ".
+ "be related to bug #19741 in %4, which describes a ".
+ "fake '%5' which generates a '%6' file instead of a '%7'. ".
+ "You could try to work around this issue by setting '%8' ".
+ "to false and trying again. This will attempt to use the ".
+ "'%9' instead.",
+ "$^X ".MAKEFILE_PL->(), MAKEFILE->(), BUILD_PL->(),
+ 'Module::Build', MAKEFILE_PL->(), 'Build', MAKEFILE->(),
+ 'prefer_makefile', BUILD_PL->()
+ ));
+
+ $fail++, last RUN;
+ }
+
+ ### if we got here, we managed to make a 'makefile' ###
+ $dist->status->makefile( MAKEFILE->($dir) );
+
+ ### start resolving prereqs ###
+ my $prereqs = $self->status->prereqs;
+
+ ### a hashref of prereqs on success, undef on failure ###
+ $prereqs ||= $dist->_find_prereqs(
+ verbose => $verbose,
+ file => $dist->status->makefile
+ );
+
+ unless( $prereqs ) {
+ error( loc( "Unable to scan '%1' for prereqs",
+ $dist->status->makefile ) );
+
+ $fail++; last RUN;
+ }
+ }
+
+ unless( $cb->_chdir( dir => $orig ) ) {
+ error( loc( "Could not chdir back to start dir '%1'", $orig ) );
+ }
+
+ ### save where we wrote this stuff -- same as extract dir in normal
+ ### installer circumstances
+ $dist->status->distdir( $self->status->extract );
+
+ return $dist->status->prepared( $fail ? 0 : 1);
+}
+
+=pod
+
+=head2 $href = $dist->_find_prereqs( file => '/path/to/Makefile', [verbose => BOOL])
+
+Parses a C<Makefile> for C<PREREQ_PM> entries and distills from that
+any prerequisites mentioned in the C<Makefile>
+
+Returns a hash with module-version pairs on success and false on
+failure.
+
+=cut
+
+sub _find_prereqs {
+ my $dist = shift;
+ my $self = $dist->parent;
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my ($verbose, $file);
+ my $tmpl = {
+ verbose => { default => $conf->get_conf('verbose'), store => \$verbose },
+ file => { required => 1, allow => FILE_READABLE, store => \$file },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ my $fh = FileHandle->new();
+ unless( $fh->open( $file ) ) {
+ error( loc( "Cannot open '%1': %2", $file, $! ) );
+ return;
+ }
+
+ my %p;
+ while( <$fh> ) {
+ my ($found) = m|^[\#]\s+PREREQ_PM\s+=>\s+(.+)|;
+
+ next unless $found;
+
+ while( $found =~ m/(?:\s)([\w\:]+)=>(?:q\[(.*?)\],?|undef)/g ) {
+ if( defined $p{$1} ) {
+ msg(loc("Warning: PREREQ_PM mentions '%1' more than once. " .
+ "Last mention wins.", $1 ), $verbose );
+ }
+
+ $p{$1} = $cb->_version_to_number(version => $2);
+ }
+ last;
+ }
+
+ my $href = $cb->_callbacks->filter_prereqs->( $cb, \%p );
+
+ $self->status->prereqs( $href );
+
+ ### just to make sure it's not the same reference ###
+ return { %$href };
+}
+
+=pod
+
+=head2 $bool = $dist->create([perl => '/path/to/perl', make => '/path/to/make', makeflags => 'EXTRA=FLAGS', prereq_target => TARGET, skiptest => BOOL, force => BOOL, verbose => BOOL])
+
+C<create> creates the files necessary for installation. This means
+it will run C<make> and C<make test>. This will also scan for and
+attempt to satisfy any prerequisites the module may have.
+
+If you set C<skiptest> to true, it will skip the C<make test> stage.
+If you set C<force> to true, it will go over all the stages of the
+C<make> process again, ignoring any previously cached results. It
+will also ignore a bad return value from C<make test> and still allow
+the operation to return true.
+
+Returns true on success and false on failure.
+
+You may then call C<< $dist->install >> on the object to actually
+install it.
+
+=cut
+
+sub create {
+ ### just in case you already did a create call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+
+ ### we're also the cpan_dist, since we don't need to have anything
+ ### prepared
+ $dist = $self->status->dist_cpan if $self->status->dist_cpan;
+ $self->status->dist_cpan( $dist ) unless $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $dir;
+ unless( $dir = $self->status->extract ) {
+ error( loc( "No dir found to operate on!" ) );
+ return;
+ }
+
+ my $args;
+ my( $force, $verbose, $make, $makeflags, $skiptest, $prereq_target, $perl,
+ $mmflags, $prereq_format, $prereq_build);
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+ my $tmpl = {
+ perl => { default => $^X, store => \$perl },
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ make => { default => $conf->get_program('make'),
+ store => \$make },
+ makeflags => { default => $conf->get_conf('makeflags'),
+ store => \$makeflags },
+ skiptest => { default => $conf->get_conf('skiptest'),
+ store => \$skiptest },
+ prereq_target => { default => '', store => \$prereq_target },
+ ### don't set the default prereq format to 'makemaker' -- wrong!
+ prereq_format => { #default => $self->status->installer_type,
+ default => '',
+ store => \$prereq_format },
+ prereq_build => { default => 0, store => \$prereq_build },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ ### maybe we already ran a create on this object? ###
+ return 1 if $dist->status->created && !$force;
+
+ ### store the arguments, so ->install can use them in recursive loops ###
+ $dist->status->_create_args( $args );
+
+ unless( $dist->status->prepared ) {
+ error( loc( "You have not successfully prepared a '%2' distribution ".
+ "yet -- cannot create yet", __PACKAGE__ ) );
+ return;
+ }
+
+
+ ### chdir to work directory ###
+ my $orig = cwd();
+ unless( $cb->_chdir( dir => $dir ) ) {
+ error( loc( "Could not chdir to build directory '%1'", $dir ) );
+ return;
+ }
+
+ my $fail; my $prereq_fail; my $test_fail;
+ RUN: {
+ ### this will set the directory back to the start
+ ### dir, so we must chdir /again/
+ my $ok = $dist->_resolve_prereqs(
+ format => $prereq_format,
+ verbose => $verbose,
+ prereqs => $self->status->prereqs,
+ target => $prereq_target,
+ force => $force,
+ prereq_build => $prereq_build,
+ );
+
+ unless( $cb->_chdir( dir => $dir ) ) {
+ error( loc( "Could not chdir to build directory '%1'", $dir ) );
+ return;
+ }
+
+ unless( $ok ) {
+
+ #### use $dist->flush to reset the cache ###
+ error( loc( "Unable to satisfy prerequisites for '%1' " .
+ "-- aborting install", $self->module ) );
+ $dist->status->make(0);
+ $fail++; $prereq_fail++;
+ last RUN;
+ }
+ ### end of prereq resolving ###
+
+ my $captured;
+
+ ### 'make' section ###
+ if( -d BLIB->($dir) && (-M BLIB->($dir) < -M $dir) && !$force ) {
+ msg(loc("Already ran '%1' for this module [%2] -- " .
+ "not running again unless you force",
+ $make, $self->module ), $verbose );
+ } else {
+ unless(scalar run( command => [$make, $makeflags],
+ buffer => \$captured,
+ verbose => $verbose )
+ ) {
+ error( loc( "MAKE failed: %1 %2", $!, $captured ) );
+ $dist->status->make(0);
+ $fail++; last RUN;
+ }
+
+ ### put the output on the stack, don't print it
+ msg( $captured, 0 );
+
+ $dist->status->make(1);
+
+ ### add this directory to your lib ###
+ $self->add_to_includepath();
+
+ ### dont bail out here, there's a conditional later on
+ #last RUN if $skiptest;
+ }
+
+ ### 'make test' section ###
+ unless( $skiptest ) {
+
+ ### turn off our PERL5OPT so no modules from CPANPLUS::inc get
+ ### included in make test -- it should build without
+ ### also, modules that run in taint mode break if we leave
+ ### our code ref in perl5opt
+ ### XXX CPANPLUS::inc functionality is now obsolete.
+ #local $ENV{PERL5OPT} = CPANPLUS::inc->original_perl5opt || '';
+
+ ### you can turn off running this verbose by changing
+ ### the config setting below, although it is really not
+ ### recommended
+ my $run_verbose =
+ $verbose ||
+ $conf->get_conf('allow_build_interactivity') ||
+ 0;
+
+ ### XXX need to add makeflags here too?
+ ### yes, but they should really be split out -- see bug #4143
+ if( scalar run(
+ command => [$make, 'test', $makeflags],
+ buffer => \$captured,
+ verbose => $run_verbose,
+ ) ) {
+ ### tests might pass because it doesn't have any tests defined
+ ### log this occasion non-verbosely, so our test reporter can
+ ### pick up on this
+ if ( NO_TESTS_DEFINED->( $captured ) ) {
+ msg( NO_TESTS_DEFINED->( $captured ), 0 )
+ } else {
+ msg( loc( "MAKE TEST passed: %2", $captured ), $verbose );
+ }
+
+ $dist->status->test(1);
+ } else {
+ error( loc( "MAKE TEST failed: %1 %2", $!, $captured ) );
+
+ ### send out error report here? or do so at a higher level?
+ ### --higher level --kane.
+ $dist->status->test(0);
+
+ ### mark specifically *test* failure.. so we dont
+ ### send success on force...
+ $test_fail++;
+
+ unless( $force ) {
+ $fail++; last RUN;
+ }
+ }
+ }
+ } #</RUN>
+
+ unless( $cb->_chdir( dir => $orig ) ) {
+ error( loc( "Could not chdir back to start dir '%1'", $orig ) );
+ }
+
+ ### send out test report?
+ ### only do so if the failure is this module, not its prereq
+ if( $conf->get_conf('cpantest') and not $prereq_fail) {
+ $cb->_send_report(
+ module => $self,
+ failed => $test_fail || $fail,
+ buffer => CPANPLUS::Error->stack_as_string,
+ verbose => $verbose,
+ force => $force,
+ ) or error(loc("Failed to send test report for '%1'",
+ $self->module ) );
+ }
+
+ return $dist->status->created( $fail ? 0 : 1);
+}
+
+=pod
+
+=head2 $bool = $dist->install([make => '/path/to/make', makemakerflags => 'EXTRA=FLAGS', force => BOOL, verbose => BOOL])
+
+C<install> runs the following command:
+ make install
+
+Returns true on success, false on failure.
+
+=cut
+
+sub install {
+
+ ### just in case you did the create with ANOTHER dist object linked
+ ### to the same module object
+ my $dist = shift();
+ my $self = $dist->parent;
+ $dist = $self->status->dist_cpan if $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+
+ unless( $dist->status->created ) {
+ error(loc("You have not successfully created a '%2' distribution yet " .
+ "-- cannot install yet", __PACKAGE__ ));
+ return;
+ }
+
+ my $dir;
+ unless( $dir = $self->status->extract ) {
+ error( loc( "No dir found to operate on!" ) );
+ return;
+ }
+
+ my $args;
+ my($force,$verbose,$make,$makeflags);
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+ my $tmpl = {
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ make => { default => $conf->get_program('make'),
+ store => \$make },
+ makeflags => { default => $conf->get_conf('makeflags'),
+ store => \$makeflags },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ ### value set and false -- means failure ###
+ if( defined $self->status->installed &&
+ !$self->status->installed && !$force
+ ) {
+ error( loc( "Module '%1' has failed to install before this session " .
+ "-- aborting install", $self->module ) );
+ return;
+ }
+
+
+ $dist->status->_install_args( $args );
+
+ my $orig = cwd();
+ unless( $cb->_chdir( dir => $dir ) ) {
+ error( loc( "Could not chdir to build directory '%1'", $dir ) );
+ return;
+ }
+
+ my $fail; my $captured;
+
+ ### 'make install' section ###
+ ### XXX need makeflags here too?
+ ### yes, but they should really be split out.. see bug #4143
+ my $cmd = [$make, 'install', $makeflags];
+ my $sudo = $conf->get_program('sudo');
+ unshift @$cmd, $sudo if $sudo and $>;
+
+ $cb->flush('lib');
+ unless(scalar run( command => $cmd,
+ verbose => $verbose,
+ buffer => \$captured,
+ ) ) {
+ error( loc( "MAKE INSTALL failed: %1 %2", $!, $captured ) );
+ $fail++;
+ }
+
+ ### put the output on the stack, don't print it
+ msg( $captured, 0 );
+
+ unless( $cb->_chdir( dir => $orig ) ) {
+ error( loc( "Could not chdir back to start dir '%1'", $orig ) );
+ }
+
+ return $dist->status->installed( $fail ? 0 : 1 );
+
+}
+
+=pod
+
+=head2 $bool = $dist->write_makefile_pl([force => BOOL, verbose => BOOL])
+
+This routine can write a C<Makefile.PL> from the information in a
+module object. It is used to write a C<Makefile.PL> when the original
+author forgot it (!!).
+
+Returns 1 on success and false on failure.
+
+The file gets written to the directory the module's been extracted
+to.
+
+=cut
+
+sub write_makefile_pl {
+ ### just in case you already did a call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+ $dist = $self->status->dist_cpan if $self->status->dist_cpan;
+ $self->status->dist_cpan( $dist ) unless $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $dir;
+ unless( $dir = $self->status->extract ) {
+ error( loc( "No dir found to operate on!" ) );
+ return;
+ }
+
+ my ($force, $verbose);
+ my $tmpl = {
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ my $file = MAKEFILE_PL->($dir);
+ if( -s $file && !$force ) {
+ msg(loc("Already created '%1' - not doing so again without force",
+ $file ), $verbose );
+ return 1;
+ }
+
+ ### due to a bug with AS perl 5.8.4 built 810 (and maybe others)
+ ### opening files with content in them already does nasty things;
+ ### seek to pos 0 and then print, but not truncating the file
+ ### bug reported to activestate on 19 sep 2004:
+ ### http://bugs.activestate.com/show_bug.cgi?id=34051
+ unlink $file if $force;
+
+ my $fh = new FileHandle;
+ unless( $fh->open( ">$file" ) ) {
+ error( loc( "Could not create file '%1': %2", $file, $! ) );
+ return;
+ }
+
+ my $mf = MAKEFILE_PL->();
+ my $name = $self->module;
+ my $version = $self->version;
+ my $author = $self->author->author;
+ my $href = $self->status->prereqs;
+ my $prereqs = join ",\n", map {
+ (' ' x 25) . "'$_'\t=> '$href->{$_}'"
+ } keys %$href;
+ $prereqs ||= ''; # just in case there are none;
+
+ print $fh qq|
+ ### Auto-generated $mf by CPANPLUS ###
+
+ use ExtUtils::MakeMaker;
+
+ WriteMakefile(
+ NAME => '$name',
+ VERSION => '$version',
+ AUTHOR => '$author',
+ PREREQ_PM => {
+$prereqs
+ },
+ );
+ \n|;
+
+ $fh->close;
+ return 1;
+}
+
+sub dist_dir {
+ ### just in case you already did a call for this module object
+ ### just via a different dist object
+ my $dist = shift;
+ my $self = $dist->parent;
+ $dist = $self->status->dist_cpan if $self->status->dist_cpan;
+ $self->status->dist_cpan( $dist ) unless $self->status->dist_cpan;
+
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $make; my $verbose;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+ my $tmpl = {
+ make => { default => $conf->get_program('make'),
+ store => \$make },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ };
+
+ check( $tmpl, \%hash ) or return;
+ }
+
+
+ my $dir;
+ unless( $dir = $self->status->extract ) {
+ error( loc( "No dir found to operate on!" ) );
+ return;
+ }
+
+ ### chdir to work directory ###
+ my $orig = cwd();
+ unless( $cb->_chdir( dir => $dir ) ) {
+ error( loc( "Could not chdir to build directory '%1'", $dir ) );
+ return;
+ }
+
+ my $fail; my $distdir;
+ TRY: {
+ $dist->prepare( @_ ) or (++$fail, last TRY);
+
+
+ my $captured;
+ unless(scalar run( command => [$make, 'distdir'],
+ buffer => \$captured,
+ verbose => $verbose )
+ ) {
+ error( loc( "MAKE DISTDIR failed: %1 %2", $!, $captured ) );
+ ++$fail, last TRY;
+ }
+
+ ### /path/to/Foo-Bar-1.2/Foo-Bar-1.2
+ $distdir = File::Spec->catdir( $dir, $self->package_name . '-' .
+ $self->package_version );
+
+ unless( -d $distdir ) {
+ error(loc("Do not know where '%1' got created", 'distdir'));
+ ++$fail, last TRY;
+ }
+ }
+
+ unless( $cb->_chdir( dir => $orig ) ) {
+ error( loc( "Could not chdir to start directory '%1'", $orig ) );
+ return;
+ }
+
+ return if $fail;
+ return $distdir;
+}
+
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Dist/Sample.pm b/lib/CPANPLUS/Dist/Sample.pm
new file mode 100644
index 0000000000..0b0939208f
--- /dev/null
+++ b/lib/CPANPLUS/Dist/Sample.pm
@@ -0,0 +1,16 @@
+package CPANPLUS::Dist::Sample;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Dist::Sample -- Sample code to create your own Dist::* plugin
+
+=head1 Description.
+
+This document is B<Obsolete>. Please read the documentation and code
+in C<CPANPLUS::Dist::Base>.
+
+=cut
+
+1;
diff --git a/lib/CPANPLUS/Error.pm b/lib/CPANPLUS/Error.pm
new file mode 100644
index 0000000000..38710a8a85
--- /dev/null
+++ b/lib/CPANPLUS/Error.pm
@@ -0,0 +1,201 @@
+package CPANPLUS::Error;
+
+use strict;
+
+use Log::Message private => 0;;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Error
+
+=head1 SYNOPSIS
+
+ use CPANPLUS::Error qw[cp_msg cp_error];
+
+=head1 DESCRIPTION
+
+This module provides the error handling code for the CPANPLUS
+libraries, and is mainly intended for internal use.
+
+=head1 FUNCTIONS
+
+=head2 cp_msg("message string" [,VERBOSE])
+
+Records a message on the stack, and prints it to C<STDOUT> (or actually
+C<$MSG_FH>, see the C<GLOBAL VARIABLES> section below), if the
+C<VERBOSE> option is true.
+The C<VERBOSE> option defaults to false.
+
+=head2 msg()
+
+An alias for C<cp_msg>.
+
+=head2 cp_error("error string" [,VERBOSE])
+
+Records an error on the stack, and prints it to C<STDERR> (or actually
+C<$ERROR_FH>, see the C<GLOBAL VARIABLES> sections below), if the
+C<VERBOSE> option is true.
+The C<VERBOSE> options defaults to true.
+
+=head2 error()
+
+An alias for C<cp_error>.
+
+=head1 CLASS METHODS
+
+=head2 CPANPLUS::Error->stack()
+
+Retrieves all the items on the stack. Since C<CPANPLUS::Error> is
+implemented using C<Log::Message>, consult its manpage for the
+function C<retrieve> to see what is returned and how to use the items.
+
+=head2 CPANPLUS::Error->stack_as_string([TRACE])
+
+Returns the whole stack as a printable string. If the C<TRACE> option is
+true all items are returned with C<Carp::longmess> output, rather than
+just the message.
+C<TRACE> defaults to false.
+
+=head2 CPANPLUS::Error->flush()
+
+Removes all the items from the stack and returns them. Since
+C<CPANPLUS::Error> is implemented using C<Log::Message>, consult its
+manpage for the function C<retrieve> to see what is returned and how
+to use the items.
+
+=cut
+
+BEGIN {
+ use Exporter;
+ use Params::Check qw[check];
+ use vars qw[@EXPORT @ISA $ERROR_FH $MSG_FH];
+
+ @ISA = 'Exporter';
+ @EXPORT = qw[cp_error cp_msg error msg];
+
+ my $log = new Log::Message;
+
+ for my $func ( @EXPORT ) {
+ no strict 'refs';
+
+ my $prefix = 'cp_';
+ my $name = $func;
+ $name =~ s/^$prefix//g;
+
+ *$func = sub {
+ my $msg = shift;
+
+ ### no point storing non-messages
+ return unless defined $msg;
+
+ $log->store(
+ message => $msg,
+ tag => uc $name,
+ level => $prefix . $name,
+ extra => [@_]
+ );
+ };
+ }
+
+ sub flush {
+ return reverse $log->flush;
+ }
+
+ sub stack {
+ return $log->retrieve( chrono => 1 );
+ }
+
+ sub stack_as_string {
+ my $class = shift;
+ my $trace = shift() ? 1 : 0;
+
+ return join $/, map {
+ '[' . $_->tag . '] [' . $_->when . '] ' .
+ ($trace ? $_->message . ' ' . $_->longmess
+ : $_->message);
+ } __PACKAGE__->stack;
+ }
+}
+
+=head1 GLOBAL VARIABLES
+
+=over 4
+
+=item $ERROR_FH
+
+This is the filehandle all the messages sent to C<error()> are being
+printed. This defaults to C<*STDERR>.
+
+=item $MSG_FH
+
+This is the filehandle all the messages sent to C<msg()> are being
+printed. This default to C<*STDOUT>.
+
+=cut
+local $| = 1;
+$ERROR_FH = \*STDERR;
+$MSG_FH = \*STDOUT;
+
+package Log::Message::Handlers;
+use Carp ();
+
+{
+
+ sub cp_msg {
+ my $self = shift;
+ my $verbose = shift;
+
+ ### so you don't want us to print the msg? ###
+ return if defined $verbose && $verbose == 0;
+
+ my $old_fh = select $CPANPLUS::Error::MSG_FH;
+
+ print '['. $self->tag . '] ' . $self->message . "\n";
+ select $old_fh;
+
+ return;
+ }
+
+ sub cp_error {
+ my $self = shift;
+ my $verbose = shift;
+
+ ### so you don't want us to print the error? ###
+ return if defined $verbose && $verbose == 0;
+
+ my $old_fh = select $CPANPLUS::Error::ERROR_FH;
+
+ ### is only going to be 1 for now anyway ###
+ ### C::I may not be loaded, so do a can() check first
+ my $cb = CPANPLUS::Internals->can('_return_all_objects')
+ ? (CPANPLUS::Internals->_return_all_objects)[0]
+ : undef;
+
+ ### maybe we didn't initialize an internals object (yet) ###
+ my $debug = $cb ? $cb->configure_object->get_conf('debug') : 0;
+ my $msg = '['. $self->tag . '] ' . $self->message . "\n";
+
+ ### i'm getting this warning in the test suite:
+ ### Ambiguous call resolved as CORE::warn(), qualify as such or
+ ### use & at CPANPLUS/Error.pm line 57.
+ ### no idea where it's coming from, since there's no 'sub warn'
+ ### anywhere to be found, but i'll mark it explicitly nonetheless
+ ### --kane
+ print $debug ? Carp::shortmess($msg) : $msg . "\n";
+
+ select $old_fh;
+
+ return;
+ }
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/FAQ.pod b/lib/CPANPLUS/FAQ.pod
new file mode 100644
index 0000000000..82bb57aaf4
--- /dev/null
+++ b/lib/CPANPLUS/FAQ.pod
@@ -0,0 +1,30 @@
+=pod
+
+=head1 NAME
+
+CPANPLUS::FAQ
+
+=head1 DESCRIPTION
+
+This document attempts to provide answers to commonly asked questions.
+
+ XXX Work in progress
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/CPANPLUS/Hacking.pod b/lib/CPANPLUS/Hacking.pod
new file mode 100644
index 0000000000..c89a403966
--- /dev/null
+++ b/lib/CPANPLUS/Hacking.pod
@@ -0,0 +1,142 @@
+=pod
+
+=head1 NAME
+
+CPANPLUS::Hacking
+
+=head1 DESCRIPTION
+
+This document attempts to describe how to easiest develop with the
+CPANPLUS environment, how certain things work and why.
+
+This is basically a quick-start guide to people who want to add
+features or patches to CPANPLUS.
+
+=head1 OBTAINING CPANPLUS
+
+CPANPLUS offers snapshots from the stable and unstable branches.
+After every patch to either of the branches, the snapshot is
+automatically updated.
+
+You can find the stable branch here (which should be equal to the
+CPAN release): L<http://p4.elixus.org/snap/cpanplus-dist.tar.gz>
+
+And the development branch here:
+L<http://p4.elixus.org/snap/cpanplus-devel.tar.gz>
+
+=head1 INSTALLING CPANPLUS
+
+CPANPLUS follows the standard perl module installation process:
+
+ perl Makefile.PL
+ make
+ make test
+ make install
+
+=head1 CONFIGURING CPANPLUS
+
+When running C<perl Makefile.PL> you will be prompted to configure.
+If you have already done so, and merely wish to update the C<Makefile>,
+simply run:
+
+ perl Makefile.PL JFDI=1
+
+This will keep your configuration intact. Note however, if there are
+changes to the default configuration file C<Config.pm-orig>, you should
+either delete your current config file and reconfigure, or patch your
+config file from the new entries in C<Config.pm-orig>.
+
+=head1 RUNNING CPANPLUS FROM DEVELOPMENT ENVIRONMENT
+
+If you'd rather not install the development version to your
+C<site_perl> directory, that's no problem. You can set your C<PERL5LIB>
+environment variable to CPANPLUS' C<lib> directory, and you can run it
+from there.
+
+=head1 RUNNING CPANPLUS TESTS
+
+Tests are what tells us if CPANPLUS is working. If a test is not working,
+try to run it explicilty like this:
+
+ perl -I/path/to/cpanplus/lib t/XX_name_of_test.t 1
+
+The extra '1' makes sure that all the messages and errors (they might
+be errors we're testing for!) are being printed rather than kept quiet.
+This is a great way to find out the context of any failures that may
+occur.
+
+If you believe this test failure proves a bug in CPANPLUS, the long
+output of the test file is something we'd like to see alongside your
+bug report.
+
+=head1 FINDING BUGS
+
+Sometimes you might find bugs in CPANPLUS' behaviour. If you encounter
+these in a development snapshot, we'd appreciate a complete patch (as
+described below in the L<SENDING PATCHES> section.
+
+If it's way over your head, then of course reporting the bug is always
+better than not reporting it at all. Before you do so though, make
+sure you have the B<latest> development snapshot, and the bug still
+persists there. If so, report the bug to this address:
+
+ cpanplus-devel@lists.sourceforge.net
+
+A good C<patch> would have the following characteristics:
+
+=over 4
+
+=item Problem description
+
+Describe clearly what the bug is you found, and what it should have
+done instead.
+
+=item Program demonstrating the bug
+
+Show us how to reproduce the bug, in a simple of a program as possible
+
+=item [OPTIONAL] A patch to the test suite to test for the bug
+
+Amend our test suite by making sure this bug will be found in this, and
+future versions of CPANPLUS (see L<SUPPLYING PATCHES>)
+
+=item [OPTIONAL] A patch to the code + tests + documentation
+
+Fix the bug, update the docs & tests. That way your bug will be gone
+forever :)
+
+=back
+
+=head1 SUPPLYING PATCHES
+
+Patches are a good thing, and they are welcome. Especially if they fix
+bugs you've found along the way, or that others have reported.
+
+We prefer patches in the following format:
+
+=over 4
+
+=item * In C<diff -u> or C<diff -c> format
+
+=item * From the root of the snapshot
+
+=item * Including patches for code + tests + docs
+
+=item * Sent per mail to cpanplus-devel@lists.sourceforge.net
+
+=item * With subject containing C<[PATCH]> + description of the patch
+
+=back
+
+You will always be informed if a patch is applied or rejected, and in
+case of rejection why that is (perhaps you can tweak the patch to have
+it accepted after all).
+
+=cut
+
+__END__
+
+* perl5lib
+* perl t/foo 1
+* patches to cpanplus-devel
+* snap/devel.tgz
diff --git a/lib/CPANPLUS/Internals.pm b/lib/CPANPLUS/Internals.pm
new file mode 100644
index 0000000000..0ba2529908
--- /dev/null
+++ b/lib/CPANPLUS/Internals.pm
@@ -0,0 +1,489 @@
+package CPANPLUS::Internals;
+
+### we /need/ perl5.6.1 or higher -- we use coderefs in @INC,
+### and 5.6.0 is just too buggy
+use 5.006001;
+
+use strict;
+use Config;
+
+
+use CPANPLUS::Error;
+
+use CPANPLUS::Selfupdate;
+
+use CPANPLUS::Internals::Source;
+use CPANPLUS::Internals::Extract;
+use CPANPLUS::Internals::Fetch;
+use CPANPLUS::Internals::Utils;
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Internals::Search;
+use CPANPLUS::Internals::Report;
+
+use Cwd qw[cwd];
+use Params::Check qw[check];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+use Object::Accessor;
+
+
+local $Params::Check::VERBOSE = 1;
+
+use vars qw[@ISA $VERSION];
+
+@ISA = qw[
+ CPANPLUS::Internals::Source
+ CPANPLUS::Internals::Extract
+ CPANPLUS::Internals::Fetch
+ CPANPLUS::Internals::Utils
+ CPANPLUS::Internals::Search
+ CPANPLUS::Internals::Report
+ ];
+
+$VERSION = "0.78";
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Internals
+
+=head1 SYNOPSIS
+
+ my $internals = CPANPLUS::Internals->_init( _conf => $conf );
+ my $backend = CPANPLUS::Internals->_retrieve_id( $ID );
+
+=head1 DESCRIPTION
+
+This module is the guts of CPANPLUS -- it inherits from all other
+modules in the CPANPLUS::Internals::* namespace, thus defying normal
+rules of OO programming -- but if you're reading this, you already
+know what's going on ;)
+
+Please read the C<CPANPLUS::Backend> documentation for the normal API.
+
+=head1 ACCESSORS
+
+=over 4
+
+=item _conf
+
+Get/set the configure object
+
+=item _id
+
+Get/set the id
+
+=item _lib
+
+Get/set the current @INC path -- @INC is reset to this after each
+install.
+
+=item _perl5lib
+
+Get/set the current PERL5LIB environment variable -- $ENV{PERL5LIB}
+is reset to this after each install.
+
+=cut
+
+### autogenerate accessors ###
+for my $key ( qw[_conf _id _lib _perl5lib _modules _hosts _methods _status
+ _callbacks _selfupdate]
+) {
+ no strict 'refs';
+ *{__PACKAGE__."::$key"} = sub {
+ $_[0]->{$key} = $_[1] if @_ > 1;
+ return $_[0]->{$key};
+ }
+}
+
+=pod
+
+=head1 METHODS
+
+=head2 $internals = CPANPLUS::Internals->_init( _conf => CONFIG_OBJ )
+
+C<_init> creates a new CPANPLUS::Internals object.
+
+You have to pass it a valid C<CPANPLUS::Configure> object.
+
+Returns the object on success, or dies on failure.
+
+=cut
+{ ### NOTE:
+ ### if extra callbacks are added, don't forget to update the
+ ### 02-internals.t test script with them!
+ my $callback_map = {
+ ### name default value
+ install_prerequisite => 1, # install prereqs when 'ask' is set?
+ edit_test_report => 0, # edit the prepared test report?
+ send_test_report => 1, # send the test report?
+ # munge the test report
+ munge_test_report => sub { return $_[1] },
+ # filter out unwanted prereqs
+ filter_prereqs => sub { return $_[1] },
+ };
+
+ my $status = Object::Accessor->new;
+ $status->mk_accessors(qw[pending_prereqs]);
+
+ my $callback = Object::Accessor->new;
+ $callback->mk_accessors(keys %$callback_map);
+
+ my $conf;
+ my $Tmpl = {
+ _conf => { required => 1, store => \$conf,
+ allow => IS_CONFOBJ },
+ _id => { default => '', no_override => 1 },
+ _lib => { default => [ @INC ], no_override => 1 },
+ _perl5lib => { default => $ENV{'PERL5LIB'}, no_override => 1 },
+ _authortree => { default => '', no_override => 1 },
+ _modtree => { default => '', no_override => 1 },
+ _hosts => { default => {}, no_override => 1 },
+ _methods => { default => {}, no_override => 1 },
+ _status => { default => '<empty>', no_override => 1 },
+ _callbacks => { default => '<empty>', no_override => 1 },
+ };
+
+ sub _init {
+ my $class = shift;
+ my %hash = @_;
+
+ ### temporary warning until we fix the storing of multiple id's
+ ### and their serialization:
+ ### probably not going to happen --kane
+ if( my $id = $class->_last_id ) {
+ # make it a singleton.
+ warn loc(q[%1 currently only supports one %2 object per ] .
+ q[running program], 'CPANPLUS', $class);
+
+ return $class->_retrieve_id( $id );
+ }
+
+ my $args = check($Tmpl, \%hash)
+ or die loc(qq[Could not initialize '%1' object], $class);
+
+ bless $args, $class;
+
+ $args->{'_id'} = $args->_inc_id;
+ $args->{'_status'} = $status;
+ $args->{'_callbacks'} = $callback;
+
+ ### initialize callbacks to default state ###
+ for my $name ( $callback->ls_accessors ) {
+ my $rv = ref $callback_map->{$name} ? 'sub return value' :
+ $callback_map->{$name} ? 'true' : 'false';
+
+ $args->_callbacks->$name(
+ sub { msg(loc("DEFAULT '%1' HANDLER RETURNING '%2'",
+ $name, $rv), $args->_conf->get_conf('debug'));
+ return ref $callback_map->{$name}
+ ? $callback_map->{$name}->( @_ )
+ : $callback_map->{$name};
+ }
+ );
+ }
+
+ ### create a selfupdate object
+ $args->_selfupdate( CPANPLUS::Selfupdate->new( $args ) );
+
+ ### initalize it as an empty hashref ###
+ $args->_status->pending_prereqs( {} );
+
+ ### allow for dirs to be added to @INC at runtime,
+ ### rather then compile time
+ push @INC, @{$conf->get_conf('lib')};
+
+ ### add any possible new dirs ###
+ $args->_lib( [@INC] );
+
+ $conf->_set_build( startdir => cwd() ),
+ or error( loc("couldn't locate current dir!") );
+
+ $ENV{FTP_PASSIVE} = 1, if $conf->get_conf('passive');
+
+ my $id = $args->_store_id( $args );
+
+ unless ( $id == $args->_id ) {
+ error( loc("IDs do not match: %1 != %2. Storage failed!",
+ $id, $args->_id) );
+ }
+
+ return $args;
+ }
+
+=pod
+
+=head2 $bool = $internals->_flush( list => \@caches )
+
+Flushes the designated caches from the C<CPANPLUS> object.
+
+Returns true on success, false if one or more caches could not be
+be flushed.
+
+=cut
+
+ sub _flush {
+ my $self = shift;
+ my %hash = @_;
+
+ my $aref;
+ my $tmpl = {
+ list => { required => 1, default => [],
+ strict_type => 1, store => \$aref },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ my $flag = 0;
+ for my $what (@$aref) {
+ my $cache = '_' . $what;
+
+ ### set the include paths back to their original ###
+ if( $what eq 'lib' ) {
+ $ENV{PERL5LIB} = $self->_perl5lib || '';
+ @INC = @{$self->_lib};
+
+ ### give all modules a new status object -- this is slightly
+ ### costly, but the best way to make sure all statusses are
+ ### forgotten --kane
+ } elsif ( $what eq 'modules' ) {
+ for my $modobj ( values %{$self->module_tree} ) {
+ $modobj->_flush;
+ }
+
+ ### blow away the methods cache... currently, that's only
+ ### File::Fetch's method fail list
+ } elsif ( $what eq 'methods' ) {
+
+ ### still fucking p4 :( ###
+ $File'Fetch::METHOD_FAIL = $File'Fetch::METHOD_FAIL = {};
+
+ ### blow away the m::l::c cache, so modules can be (re)loaded
+ ### again if they become available
+ } elsif ( $what eq 'load' ) {
+ undef $Module::Load::Conditional::CACHE;
+
+ } else {
+ unless ( exists $self->{$cache} && exists $Tmpl->{$cache} ) {
+ error( loc( "No such cache: '%1'", $what ) );
+ $flag++;
+ next;
+ } else {
+ $self->$cache( {} );
+ }
+ }
+ }
+ return !$flag;
+ }
+
+### NOTE:
+### if extra callbacks are added, don't forget to update the
+### 02-internals.t test script with them!
+
+=pod
+
+=head2 $bool = $internals->_register_callback( name => CALLBACK_NAME, code => CODEREF );
+
+Registers a callback for later use by the internal libraries.
+
+Here is a list of the currently used callbacks:
+
+=over 4
+
+=item install_prerequisite
+
+Is called when the user wants to be C<asked> about what to do with
+prerequisites. Should return a boolean indicating true to install
+the prerequisite and false to skip it.
+
+=item send_test_report
+
+Is called when the user should be prompted if he wishes to send the
+test report. Should return a boolean indicating true to send the
+test report and false to skip it.
+
+=item munge_test_report
+
+Is called when the test report message has been composed, giving
+the user a chance to programatically alter it. Should return the
+(munged) message to be sent.
+
+=item edit_test_report
+
+Is called when the user should be prompted to edit test reports
+about to be sent out by Test::Reporter. Should return a boolean
+indicating true to edit the test report in an editor and false
+to skip it.
+
+=back
+
+=cut
+
+ sub _register_callback {
+ my $self = shift or return;
+ my %hash = @_;
+
+ my ($name,$code);
+ my $tmpl = {
+ name => { required => 1, store => \$name,
+ allow => [$callback->ls_accessors] },
+ code => { required => 1, allow => IS_CODEREF,
+ store => \$code },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ $self->_callbacks->$name( $code ) or return;
+
+ return 1;
+ }
+
+# =head2 $bool = $internals->_add_callback( name => CALLBACK_NAME, code => CODEREF );
+#
+# Adds a new callback to be used from anywhere in the system. If the callback
+# is already known, an error is raised and false is returned. If the callback
+# is not yet known, it is added, and the corresponding coderef is registered
+# using the
+#
+# =cut
+#
+# sub _add_callback {
+# my $self = shift or return;
+# my %hash = @_;
+#
+# my ($name,$code);
+# my $tmpl = {
+# name => { required => 1, store => \$name, },
+# code => { required => 1, allow => IS_CODEREF,
+# store => \$code },
+# };
+#
+# check( $tmpl, \%hash ) or return;
+#
+# if( $callback->can( $name ) ) {
+# error(loc("Callback '%1' is already registered"));
+# return;
+# }
+#
+# $callback->mk_accessor( $name );
+#
+# $self->_register_callback( name => $name, code => $code ) or return;
+#
+# return 1;
+# }
+
+}
+
+=pod
+
+=head2 $bool = $internals->_add_to_includepath( directories => \@dirs )
+
+Adds a list of directories to the include path.
+This means they get added to C<@INC> as well as C<$ENV{PERL5LIB}>.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _add_to_includepath {
+ my $self = shift;
+ my %hash = @_;
+
+ my $dirs;
+ my $tmpl = {
+ directories => { required => 1, default => [], store => \$dirs,
+ strict_type => 1 },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ for my $lib (@$dirs) {
+ push @INC, $lib unless grep { $_ eq $lib } @INC;
+ }
+
+ { local $^W; ### it will be complaining if $ENV{PERL5LIB]
+ ### is not defined (yet).
+ $ENV{'PERL5LIB'} .= join '', map { $Config{'path_sep'} . $_ } @$dirs;
+ }
+
+ return 1;
+}
+
+=pod
+
+=head2 $id = CPANPLUS::Internals->_last_id
+
+Return the id of the last object stored.
+
+=head2 $id = CPANPLUS::Internals->_store_id( $internals )
+
+Store this object; return its id.
+
+=head2 $obj = CPANPLUS::Internals->_retrieve_id( $ID )
+
+Retrieve an object based on its ID -- return false on error.
+
+=head2 CPANPLUS::Internals->_remove_id( $ID )
+
+Remove the object marked by $ID from storage.
+
+=head2 @objs = CPANPLUS::Internals->_return_all_objects
+
+Return all stored objects.
+
+=cut
+
+
+### code for storing multiple objects
+### -- although we only support one right now
+### XXX when support for multiple objects comes, saving source will have
+### to change
+{
+ my $idref = {};
+ my $count = 0;
+
+ sub _inc_id { return ++$count; }
+
+ sub _last_id { $count }
+
+ sub _store_id {
+ my $self = shift;
+ my $obj = shift or return;
+
+ unless( IS_INTERNALS_OBJ->($obj) ) {
+ error( loc("The object you passed has the wrong ref type: '%1'",
+ ref $obj) );
+ return;
+ }
+
+ $idref->{ $obj->_id } = $obj;
+ return $obj->_id;
+ }
+
+ sub _retrieve_id {
+ my $self = shift;
+ my $id = shift or return;
+
+ my $obj = $idref->{$id};
+ return $obj;
+ }
+
+ sub _remove_id {
+ my $self = shift;
+ my $id = shift or return;
+
+ return delete $idref->{$id};
+ }
+
+ sub _return_all_objects { return values %$idref }
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Constants.pm b/lib/CPANPLUS/Internals/Constants.pm
new file mode 100644
index 0000000000..0961e25807
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Constants.pm
@@ -0,0 +1,302 @@
+package CPANPLUS::Internals::Constants;
+
+use strict;
+
+use CPANPLUS::Error;
+
+use File::Spec;
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+require Exporter;
+use vars qw[$VERSION @ISA @EXPORT];
+
+use Package::Constants;
+
+
+$VERSION = 0.01;
+@ISA = qw[Exporter];
+@EXPORT = Package::Constants->list( __PACKAGE__ );
+
+
+sub constants { @EXPORT };
+
+use constant INSTALLER_BUILD
+ => 'CPANPLUS::Dist::Build';
+use constant INSTALLER_MM => 'CPANPLUS::Dist::MM';
+use constant INSTALLER_SAMPLE
+ => 'CPANPLUS::Dist::Sample';
+use constant INSTALLER_BASE => 'CPANPLUS::Dist::Base';
+
+use constant CONFIG => 'CPANPLUS::Config';
+use constant CONFIG_USER => 'CPANPLUS::Config::User';
+use constant CONFIG_SYSTEM => 'CPANPLUS::Config::System';
+
+use constant TARGET_CREATE => 'create';
+use constant TARGET_PREPARE => 'prepare';
+use constant TARGET_INSTALL => 'install';
+use constant TARGET_IGNORE => 'ignore';
+use constant DOT_CPANPLUS => $^O eq 'VMS' ? '_cpanplus' : '.cpanplus';
+
+use constant OPT_AUTOFLUSH => '-MCPANPLUS::Internals::Utils::Autoflush';
+
+use constant UNKNOWN_DL_LOCATION
+ => 'UNKNOWN-ORIGIN';
+
+use constant NMAKE => 'nmake.exe';
+use constant NMAKE_URL =>
+ 'ftp://ftp.microsoft.com/Softlib/MSLFILES/nmake15.exe';
+
+use constant INSTALL_VIA_PACKAGE_MANAGER
+ => sub { my $fmt = $_[0] or return;
+ return 1 if $fmt ne INSTALLER_BUILD and
+ $fmt ne INSTALLER_MM;
+ };
+
+use constant IS_CODEREF => sub { ref $_[-1] eq 'CODE' };
+use constant IS_MODOBJ => sub { UNIVERSAL::isa($_[-1],
+ 'CPANPLUS::Module') };
+use constant IS_FAKE_MODOBJ => sub { UNIVERSAL::isa($_[-1],
+ 'CPANPLUS::Module::Fake') };
+use constant IS_AUTHOBJ => sub { UNIVERSAL::isa($_[-1],
+ 'CPANPLUS::Module::Author') };
+use constant IS_FAKE_AUTHOBJ
+ => sub { UNIVERSAL::isa($_[-1],
+ 'CPANPLUS::Module::Author::Fake') };
+
+use constant IS_CONFOBJ => sub { UNIVERSAL::isa($_[-1],
+ 'CPANPLUS::Configure') };
+
+use constant IS_RVOBJ => sub { UNIVERSAL::isa($_[-1],
+ 'CPANPLUS::Backend::RV') };
+
+use constant IS_INTERNALS_OBJ
+ => sub { UNIVERSAL::isa($_[-1],
+ 'CPANPLUS::Internals') };
+
+use constant IS_FILE => sub { return 1 if -e $_[-1] };
+
+use constant FILE_EXISTS => sub {
+ my $file = $_[-1];
+ return 1 if IS_FILE->($file);
+ local $Carp::CarpLevel =
+ $Carp::CarpLevel+2;
+ error(loc( q[File '%1' does not exist],
+ $file));
+ return;
+ };
+
+use constant FILE_READABLE => sub {
+ my $file = $_[-1];
+ return 1 if -e $file && -r _;
+ local $Carp::CarpLevel =
+ $Carp::CarpLevel+2;
+ error( loc( q[File '%1' is not readable ].
+ q[or does not exist], $file));
+ return;
+ };
+use constant IS_DIR => sub { return 1 if -d $_[-1] };
+
+use constant DIR_EXISTS => sub {
+ my $dir = $_[-1];
+ return 1 if IS_DIR->($dir);
+ local $Carp::CarpLevel =
+ $Carp::CarpLevel+2;
+ error(loc(q[Dir '%1' does not exist],
+ $dir));
+ return;
+ };
+
+use constant MAKEFILE_PL => sub { return @_
+ ? File::Spec->catfile( @_,
+ 'Makefile.PL' )
+ : 'Makefile.PL';
+ };
+use constant MAKEFILE => sub { return @_
+ ? File::Spec->catfile( @_,
+ 'Makefile' )
+ : 'Makefile';
+ };
+use constant BUILD_PL => sub { return @_
+ ? File::Spec->catfile( @_,
+ 'Build.PL' )
+ : 'Build.PL';
+ };
+
+use constant BLIB => sub { return @_
+ ? File::Spec->catfile(@_, 'blib')
+ : 'blib';
+ };
+
+use constant LIB => 'lib';
+use constant LIB_DIR => sub { return @_
+ ? File::Spec->catdir(@_, LIB)
+ : LIB;
+ };
+use constant AUTO => 'auto';
+use constant LIB_AUTO_DIR => sub { return @_
+ ? File::Spec->catdir(@_, LIB, AUTO)
+ : File::Spec->catdir(LIB, AUTO)
+ };
+use constant ARCH => 'arch';
+use constant ARCH_DIR => sub { return @_
+ ? File::Spec->catdir(@_, ARCH)
+ : ARCH;
+ };
+use constant ARCH_AUTO_DIR => sub { return @_
+ ? File::Spec->catdir(@_,ARCH,AUTO)
+ : File::Spec->catdir(ARCH,AUTO)
+ };
+
+use constant BLIB_LIBDIR => sub { return @_
+ ? File::Spec->catdir(
+ @_, BLIB->(), LIB )
+ : File::Spec->catdir( BLIB->(), LIB );
+ };
+
+use constant CONFIG_USER_LIB_DIR => sub {
+ require CPANPLUS::Internals::Utils;
+ LIB_DIR->(
+ CPANPLUS::Internals::Utils->_home_dir,
+ DOT_CPANPLUS
+ );
+ };
+use constant CONFIG_USER_FILE => sub {
+ File::Spec->catfile(
+ CONFIG_USER_LIB_DIR->(),
+ split('::', CONFIG_USER),
+ ) . '.pm';
+ };
+use constant CONFIG_SYSTEM_FILE => sub {
+ require CPANPLUS::Internals;
+ require File::Basename;
+ my $dir = File::Basename::dirname(
+ $INC{'CPANPLUS/Internals.pm'}
+ );
+
+ ### XXX use constants
+ File::Spec->catfile(
+ $dir, qw[Config System.pm]
+ );
+ };
+
+use constant README => sub { my $obj = $_[0];
+ my $pkg = $obj->package_name;
+ $pkg .= '-' . $obj->package_version .
+ '.readme';
+ return $pkg;
+ };
+use constant OPEN_FILE => sub {
+ my($file, $mode) = (@_, '');
+ my $fh;
+ open $fh, "$mode" . $file
+ or error(loc(
+ "Could not open file '%1': %2",
+ $file, $!));
+ return $fh if $fh;
+ return;
+ };
+
+use constant STRIP_GZ_SUFFIX
+ => sub {
+ my $file = $_[0] or return;
+ $file =~ s/.gz$//i;
+ return $file;
+ };
+
+use constant CHECKSUMS => 'CHECKSUMS';
+use constant PGP_HEADER => '-----BEGIN PGP SIGNED MESSAGE-----';
+use constant ENV_CPANPLUS_CONFIG
+ => 'PERL5_CPANPLUS_CONFIG';
+use constant ENV_CPANPLUS_IS_EXECUTING
+ => 'PERL5_CPANPLUS_IS_EXECUTING';
+use constant DEFAULT_EMAIL => 'cpanplus@example.com';
+use constant CPANPLUS_UA => sub { ### for the version number ###
+ require CPANPLUS::Internals;
+ "CPANPLUS/$CPANPLUS::Internals::VERSION"
+ };
+use constant TESTERS_URL => sub {
+ "http://testers.cpan.org/show/" .
+ $_[0] .".yaml"
+ };
+use constant TESTERS_DETAILS_URL
+ => sub {
+ 'http://testers.cpan.org/show/' .
+ $_[0] . '.html';
+ };
+
+use constant CREATE_FILE_URI
+ => sub {
+ my $dir = $_[0] or return;
+ return $dir =~ m|^/|
+ ? 'file:/' . $dir
+ : 'file://' . $dir;
+ };
+
+use constant DOT_SHELL_DEFAULT_RC
+ => '.shell-default.rc';
+
+use constant PREREQ_IGNORE => 0;
+use constant PREREQ_INSTALL => 1;
+use constant PREREQ_ASK => 2;
+use constant PREREQ_BUILD => 3;
+use constant BOOLEANS => [0,1];
+use constant CALLING_FUNCTION
+ => sub { my $lvl = $_[0] || 0;
+ return join '::', (caller(2+$lvl))[3]
+ };
+use constant PERL_CORE => 'perl';
+
+use constant GET_XS_FILES => sub { my $dir = $_[0] or return;
+ require File::Find;
+ my @files;
+ File::Find::find(
+ sub { push @files, $File::Find::name
+ if $File::Find::name =~ /\.xs$/i
+ }, $dir );
+
+ return @files;
+ };
+
+use constant INSTALL_LOG_FILE
+ => sub { my $obj = shift or return;
+ my $name = $obj->name; $name =~ s/::/-/g;
+ $name .= '-'. $obj->version;
+ $name .= '-'. scalar(time) . '.log';
+ return $name;
+ };
+
+use constant ON_WIN32 => $^O eq 'MSWin32';
+use constant ON_NETWARE => $^O eq 'NetWare';
+use constant ON_CYGWIN => $^O eq 'cygwin';
+use constant ON_VMS => $^O eq 'VMS';
+
+use constant ON_OLD_CYGWIN => do { ON_CYGWIN and $] < 5.008
+ ? loc(
+ "Your perl version for %1 is too low; ".
+ "Require %2 or higher for this function",
+ $^O, '5.8.0' )
+ : '';
+ };
+
+### XXX these 2 are probably obsolete -- check & remove;
+use constant DOT_EXISTS => '.exists';
+
+use constant QUOTE_PERL_ONE_LINER
+ => sub { my $line = shift or return;
+
+ ### use double quotes on these systems
+ return qq["$line"]
+ if ON_WIN32 || ON_NETWARE || ON_VMS;
+
+ ### single quotes on the rest
+ return qq['$line'];
+ };
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Constants/Report.pm b/lib/CPANPLUS/Internals/Constants/Report.pm
new file mode 100644
index 0000000000..10a14e60e3
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Constants/Report.pm
@@ -0,0 +1,357 @@
+package CPANPLUS::Internals::Constants::Report;
+
+use strict;
+use CPANPLUS::Error;
+
+use File::Spec;
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+require Exporter;
+use vars qw[$VERSION @ISA @EXPORT];
+
+use Package::Constants;
+
+
+$VERSION = 0.01;
+@ISA = qw[Exporter];
+@EXPORT = Package::Constants->list( __PACKAGE__ );
+
+### for the version
+require CPANPLUS::Internals;
+
+### OS to regex map ###
+my %OS = (
+ Amiga => 'amigaos',
+ Atari => 'mint',
+ BSD => 'bsdos|darwin|freebsd|openbsd|netbsd',
+ Be => 'beos',
+ BeOS => 'beos',
+ Cygwin => 'cygwin',
+ Darwin => 'darwin',
+ EBCDIC => 'os390|os400|posix-bc|vmesa',
+ HPUX => 'hpux',
+ Linux => 'linux',
+ MSDOS => 'dos|os2|MSWin32|cygwin',
+ 'bin\\d*Mac'=> 'MacOS|darwin', # binMac, bin56Mac, bin58Mac...
+ Mac => 'MacOS|darwin',
+ MacPerl => 'MacOS',
+ MacOS => 'MacOS|darwin',
+ MacOSX => 'darwin',
+ MPE => 'mpeix',
+ MPEiX => 'mpeix',
+ OS2 => 'os2',
+ Plan9 => 'plan9',
+ RISCOS => 'riscos',
+ SGI => 'irix',
+ Solaris => 'solaris',
+ Unix => 'aix|bsdos|darwin|dgux|dynixptx|freebsd|'.
+ 'linux|hpux|machten|netbsd|next|openbsd|dec_osf|'.
+ 'svr4|sco_sv|unicos|unicosmk|solaris|sunos',
+ VMS => 'VMS',
+ VOS => 'VOS',
+ Win32 => 'MSWin32|cygwin',
+ Win32API => 'MSWin32|cygwin',
+);
+
+use constant GRADE_FAIL => 'fail';
+use constant GRADE_PASS => 'pass';
+use constant GRADE_NA => 'na';
+use constant GRADE_UNKNOWN => 'unknown';
+
+use constant MAX_REPORT_SEND
+ => 2;
+
+use constant CPAN_TESTERS_EMAIL
+ => 'cpan-testers@perl.org';
+
+### the cpan mail account for this user ###
+use constant CPAN_MAIL_ACCOUNT
+ => sub {
+ my $username = shift or return;
+ return $username . '@cpan.org';
+ };
+
+### check if this module is platform specific and if we're on that
+### specific platform. Alternately, the module is not platform specific
+### and we're always OK to send out test results.
+use constant RELEVANT_TEST_RESULT
+ => sub {
+ my $mod = shift or return;
+ my $name = $mod->module;
+ my $specific;
+ for my $platform (keys %OS) {
+ if( $name =~ /\b$platform\b/i ) {
+ # beware the Mac != MAC
+ next if($platform eq 'Mac' &&
+ $name !~ /\b$platform\b/);
+ $specific++;
+ return 1 if
+ $^O =~ /^(?:$OS{$platform})$/
+ }
+ };
+ return $specific ? 0 : 1;
+ };
+
+use constant UNSUPPORTED_OS
+ => sub {
+ my $buffer = shift or return;
+ if( $buffer =~
+ /No support for OS|OS unsupported/im ) {
+ return 1;
+ }
+ return 0;
+ };
+
+use constant PERL_VERSION_TOO_LOW
+ => sub {
+ my $buffer = shift or return;
+ # ExtUtils::MakeMaker format
+ if( $buffer =~
+ /Perl .*? required--this is only .*?/m ) {
+ return 1;
+ }
+ # Module::Build format
+ if( $buffer =~
+ /ERROR:( perl:)? Version .*?( of perl)? is installed, but we need version >= .*?/m ) {
+ return 1;
+ }
+ return 0;
+ };
+
+use constant NO_TESTS_DEFINED
+ => sub {
+ my $buffer = shift or return;
+ if( $buffer =~
+ /(No tests defined( for [\w:]+ extension)?\.)/
+ and $buffer !~ /\*\.t/m and
+ $buffer !~ /test\.pl/m
+ ) {
+ return $1
+ }
+
+ return;
+ };
+
+### what stage did the test fail? ###
+use constant TEST_FAIL_STAGE
+ => sub {
+ my $buffer = shift or return;
+ return $buffer =~ /(MAKE [A-Z]+).*/
+ ? lc $1 :
+ 'fetch';
+ };
+
+
+use constant MISSING_PREREQS_LIST
+ => sub {
+ my $buffer = shift;
+ my @list = map { s/.pm$//; s|/|::|g; $_ }
+ ($buffer =~
+ m/\bCan\'t locate (\S+) in \@INC/g);
+
+ ### make sure every missing prereq is only
+ ### listed ones
+ { my %seen;
+ @list = grep { !$seen{$_}++ } @list
+ }
+
+ return @list;
+ };
+
+use constant MISSING_EXTLIBS_LIST
+ => sub {
+ my $buffer = shift;
+ my @list =
+ ($buffer =~
+ m/No library found for -l([-\w]+)/g);
+
+ return @list;
+ };
+
+use constant REPORT_MESSAGE_HEADER
+ => sub {
+ my ($version, $author) = @_;
+ return << ".";
+
+Dear $author,
+
+This is a computer-generated error report created automatically by
+CPANPLUS, version $version. Testers personal comments may appear
+at the end of this report.
+
+.
+ };
+
+use constant REPORT_MESSAGE_FAIL_HEADER
+ => sub {
+ my($stage, $buffer) = @_;
+ return << ".";
+
+Thank you for uploading your work to CPAN. However, it appears that
+there were some problems testing your distribution.
+
+TEST RESULTS:
+
+Below is the error stack from stage '$stage':
+
+$buffer
+
+.
+ };
+
+use constant REPORT_MISSING_PREREQS
+ => sub {
+ my ($author,$email,@missing) = @_;
+ $author = ($author && $email)
+ ? "$author ($email)"
+ : 'Your Name Here';
+
+ my $modules = join "\n", @missing;
+ my $prereqs = join "\n",
+ map {"\t'$_'\t=> '0',".
+ " # or a minimum working version"}
+ @missing;
+
+ return << ".";
+
+MISSING PREREQUISITES:
+
+It was observed that the test suite seem to fail without these modules:
+
+$modules
+
+As such, adding the prerequisite module(s) to 'PREREQ_PM' in your
+Makefile.PL should solve this problem. For example:
+
+WriteMakefile(
+ AUTHOR => '$author',
+ ... # other information
+ PREREQ_PM => {
+$prereqs
+ }
+);
+
+If you are interested in making a more flexible Makefile.PL that can
+probe for missing dependencies and install them, ExtUtils::AutoInstall
+at <http://search.cpan.org/dist/ExtUtils-AutoInstall/> may be
+worth a look.
+
+Thanks! :-)
+
+.
+ };
+
+use constant REPORT_MISSING_TESTS
+ => sub {
+ return << ".";
+RECOMMENDATIONS:
+
+It would be very helpful if you could include even a simple test
+script in the next release, so people can verify which platforms
+can successfully install them, as well as avoid regression bugs?
+
+A simple 't/use.t' that says:
+
+#!/usr/bin/env perl -w
+use strict;
+use Test;
+BEGIN { plan tests => 1 }
+
+use Your::Module::Here; ok(1);
+exit;
+__END__
+
+would be appreciated. If you are interested in making a more robust
+test suite, please see the Test::Simple, Test::More and Test::Tutorial
+documentation at <http://search.cpan.org/dist/Test-Simple/>.
+
+Thanks! :-)
+
+.
+ };
+
+use constant REPORT_LOADED_PREREQS
+ => sub {
+ my $mod = shift;
+ my $cb = $mod->parent;
+ my $prq = $mod->status->prereqs || {};
+
+ ### not every prereq may be coming from CPAN
+ ### so maybe we wont find it in our module
+ ### tree at all...
+ ### skip ones that cant be found in teh list
+ ### as reported in #12723
+ my @prq = grep { defined }
+ map { $cb->module_tree($_) }
+ sort keys %$prq;
+
+ ### no prereqs?
+ return '' unless @prq;
+
+ ### some apparently, list what we loaded
+ my $str = << ".";
+PREREQUISITES:
+
+Here is a list of prerequisites you specified and versions we
+managed to load:
+
+.
+ $str .= join '',
+ map { my $want = $prq->{$_->name};
+
+ sprintf "\t%s %-30s %8s %8s\n",
+ do { $_->is_uptodate(
+ version => $want
+ ) ? ' ' : '!'
+ },
+ $_->name,
+ $_->installed_version,
+ $want
+
+ ### might be empty entries in there
+ } grep { defined $_ } @prq;
+
+ return $str;
+ };
+
+use constant REPORT_TESTS_SKIPPED
+ => sub {
+ return << ".";
+
+******************************** NOTE ********************************
+*** ***
+*** The tests for this module were skipped during this build ***
+*** ***
+**********************************************************************
+
+.
+ };
+
+use constant REPORT_MESSAGE_FOOTER
+ => sub {
+ return << ".";
+
+******************************** NOTE ********************************
+The comments above are created mechanically, possibly without manual
+checking by the sender. As there are many people performing automatic
+tests on each upload to CPAN, it is likely that you will receive
+identical messages about the same problem.
+
+If you believe that the message is mistaken, please reply to the first
+one with correction and/or additional informations, and do not take
+it personally. We appreciate your patience. :)
+**********************************************************************
+
+Additional comments:
+
+.
+ };
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Extract.pm b/lib/CPANPLUS/Internals/Extract.pm
new file mode 100644
index 0000000000..544d5894f9
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Extract.pm
@@ -0,0 +1,236 @@
+package CPANPLUS::Internals::Extract;
+
+use strict;
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+use File::Spec ();
+use File::Basename ();
+use Archive::Extract;
+use IPC::Cmd qw[run];
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load check_install];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+local $Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Internals::Extract
+
+=head1 SYNOPSIS
+
+ ### for source files ###
+ $self->_gunzip( file => 'foo.gz', output => 'blah.txt' );
+
+ ### for modules/packages ###
+ $dir = $self->_extract( module => $modobj,
+ extractdir => '/some/where' );
+
+=head1 DESCRIPTION
+
+CPANPLUS::Internals::Extract extracts compressed files for CPANPLUS.
+It can do this by either a pure perl solution (preferred) with the
+use of C<Archive::Tar> and C<Compress::Zlib>, or with binaries, like
+C<gzip> and C<tar>.
+
+The flow looks like this:
+
+ $cb->_extract
+ Delegate to Archive::Extract
+
+=head1 METHODS
+
+=head2 $dir = _extract( module => $modobj, [perl => '/path/to/perl', extractdir => '/path/to/extract/to', prefer_bin => BOOL, verbose => BOOL, force => BOOL] )
+
+C<_extract> will take a module object and extract it to C<extractdir>
+if provided, or the default location which is obtained from your
+config.
+
+The file name is obtained by looking at C<< $modobj->status->fetch >>
+and will be parsed to see if it's a tar or zip archive.
+
+If it's a zip archive, C<__unzip> will be called, otherwise C<__untar>
+will be called. In the unlikely event the file is of neither format,
+an error will be thrown.
+
+C<_extract> takes the following options:
+
+=over 4
+
+=item module
+
+A C<CPANPLUS::Module> object. This is required.
+
+=item extractdir
+
+The directory to extract the archive to. By default this looks
+something like:
+ /CPANPLUS_BASE/PERL_VERSION/BUILD/MODULE_NAME
+
+=item prefer_bin
+
+A flag indicating whether you prefer a pure perl solution, ie
+C<Archive::Tar> or C<Archive::Zip> respectively, or a binary solution
+like C<unzip> and C<tar>.
+
+=item perl
+
+The path to the perl executable to use for any perl calls. Also used
+to determine the build version directory for extraction.
+
+=item verbose
+
+Specifies whether to be verbose or not. Defaults to your corresponding
+config entry.
+
+=item force
+
+Specifies whether to force the extraction or not. Defaults to your
+corresponding config entry.
+
+=back
+
+All other options are passed on verbatim to C<__unzip> or C<__untar>.
+
+Returns the directory the file was extracted to on success and false
+on failure.
+
+=cut
+
+sub _extract {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my( $mod, $verbose, $force );
+ my $tmpl = {
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ prefer_bin => { default => $conf->get_conf('prefer_bin') },
+ extractdir => { default => $conf->get_conf('extractdir') },
+ module => { required => 1, allow => IS_MODOBJ, store => \$mod },
+ perl => { default => $^X },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### did we already extract it ? ###
+ my $loc = $mod->status->extract();
+
+ if( $loc && !$force ) {
+ msg(loc("Already extracted '%1' to '%2'. ".
+ "Won't extract again without force",
+ $mod->module, $loc), $verbose);
+ return $loc;
+ }
+
+ ### did we already fetch the file? ###
+ my $file = $mod->status->fetch();
+ unless( -s $file ) {
+ error( loc( "File '%1' has zero size: cannot extract", $file ) );
+ return;
+ }
+
+ ### the dir to extract to ###
+ my $to = $args->{'extractdir'} ||
+ File::Spec->catdir(
+ $conf->get_conf('base'),
+ $self->_perl_version( perl => $args->{'perl'} ),
+ $conf->_get_build('moddir'),
+ );
+
+ ### delegate to Archive::Extract ###
+ ### set up some flags for archive::extract ###
+ local $Archive::Extract::PREFER_BIN = $args->{'prefer_bin'};
+ local $Archive::Extract::DEBUG = $conf->get_conf('debug');
+ local $Archive::Extract::WARN = $verbose;
+
+ my $ae = Archive::Extract->new( archive => $file );
+
+ unless( $ae->extract( to => $to ) ) {
+ error( loc( "Unable to extract '%1' to '%2': %3",
+ $file, $to, $ae->error ) );
+ return;
+ }
+
+ ### if ->files is not filled, we dont know what the hell was
+ ### extracted.. try to offer a suggestion and bail :(
+ unless ( $ae->files ) {
+ error( loc( "'%1' was not able to determine extracted ".
+ "files from the archive. Instal '%2' and ensure ".
+ "it works properly and try again",
+ $ae->is_zip ? 'Archive::Zip' : 'Archive::Tar' ) );
+ return;
+ }
+
+
+ ### print out what files we extracted ###
+ msg(loc("Extracted '%1'",$_),$verbose) for @{$ae->files};
+
+ ### set them all to be +w for the owner, so we don't get permission
+ ### denied for overwriting files that are just +r
+
+ ### this is to rigurous -- just change to +w for the owner [cpan #13358]
+ #chmod 0755, map { File::Spec->rel2abs( File::Spec->catdir($to, $_) ) }
+ # @{$ae->files};
+
+ for my $file ( @{$ae->files} ) {
+ my $path = File::Spec->rel2abs( File::Spec->catdir($to, $file) );
+
+ $self->_mode_plus_w( file => $path );
+ }
+
+ ### check the return value for the extracted path ###
+ ### Make an educated guess if we didn't get an extract_path
+ ### back
+ ### XXX apparently some people make their own dists and they
+ ### pack up '.' which means the leading directory is '.'
+ ### and only the second directory is the actual module directory
+ ### so, we'll have to check if our educated guess exists first,
+ ### then see if the extract path works.. and if nothing works...
+ ### well, then we really don't know.
+
+ my $dir;
+ for my $try ( File::Spec->rel2abs( File::Spec->catdir(
+ $to, $mod->package_name .'-'. $mod->package_version ) ),
+ File::Spec->rel2abs( $ae->extract_path ),
+ ) {
+ ($dir = $try) && last if -d $try;
+ }
+
+ ### test if the dir exists ###
+ unless( $dir && -d $dir ) {
+ error(loc("Unable to determine extract dir for '%1'",$mod->module));
+ return;
+
+ } else {
+ msg(loc("Extracted '%1' to '%2'", $mod->module, $dir), $verbose);
+
+ ### register where we extracted the files to,
+ ### also store what files were extracted
+ $mod->status->extract( $dir );
+ $mod->status->files( $ae->files );
+ }
+
+ ### also, figure out what kind of install we're dealing with ###
+ $mod->get_installer_type();
+
+ return $mod->status->extract();
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Fetch.pm b/lib/CPANPLUS/Internals/Fetch.pm
new file mode 100644
index 0000000000..b8ad371fcc
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Fetch.pm
@@ -0,0 +1,372 @@
+package CPANPLUS::Internals::Fetch;
+
+use strict;
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+use File::Fetch;
+use File::Spec;
+use Cwd qw[cwd];
+use IPC::Cmd qw[run];
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+$Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Internals::Fetch
+
+=head1 SYNOPSIS
+
+ my $output = $cb->_fetch(
+ module => $modobj,
+ fetchdir => '/path/to/save/to',
+ verbose => BOOL,
+ force => BOOL,
+ );
+
+ $cb->_add_fail_host( host => 'foo.com' );
+ $cb->_host_ok( host => 'foo.com' );
+
+
+=head1 DESCRIPTION
+
+CPANPLUS::Internals::Fetch fetches files from either ftp, http, file
+or rsync mirrors.
+
+This is the rough flow:
+
+ $cb->_fetch
+ Delegate to File::Fetch;
+
+
+=head1 METHODS
+
+=cut
+
+=head1 $path = _fetch( module => $modobj, [fetchdir => '/path/to/save/to', fetch_from => 'scheme://path/to/fetch/from', verbose => BOOL, force => BOOL, prefer_bin => BOOL] )
+
+C<_fetch> will fetch files based on the information in a module
+object. You always need a module object. If you want a fake module
+object for a one-off fetch, look at C<CPANPLUS::Module::Fake>.
+
+C<fetchdir> is the place to save the file to. Usually this
+information comes from your configuration, but you can override it
+expressly if needed.
+
+C<fetch_from> lets you specify an URI to get this file from. If you
+do not specify one, your list of configured hosts will be probed to
+download the file from.
+
+C<force> forces a new download, even if the file already exists.
+
+C<verbose> simply indicates whether or not to print extra messages.
+
+C<prefer_bin> indicates whether you prefer the use of commandline
+programs over perl modules. Defaults to your corresponding config
+setting.
+
+C<_fetch> figures out, based on the host list, what scheme to use and
+from there, delegates to C<File::Fetch> do the actual fetching.
+
+Returns the path of the output file on success, false on failure.
+
+Note that you can set a C<blacklist> on certain methods in the config.
+Simply add the identifying name of the method (ie, C<lwp>) to:
+ $conf->_set_fetch( blacklist => ['lwp'] );
+
+And the C<LWP> function will be skipped by C<File::Fetch>.
+
+=cut
+
+sub _fetch {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ local $Params::Check::NO_DUPLICATES = 0;
+
+ my ($modobj, $verbose, $force, $fetch_from);
+ my $tmpl = {
+ module => { required => 1, allow => IS_MODOBJ, store => \$modobj },
+ fetchdir => { default => $conf->get_conf('fetchdir') },
+ fetch_from => { default => '', store => \$fetch_from },
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ prefer_bin => { default => $conf->get_conf('prefer_bin') },
+ };
+
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### check if we already downloaded the thing ###
+ if( (my $where = $modobj->status->fetch()) && !$force ) {
+ msg(loc("Already fetched '%1' to '%2', " .
+ "won't fetch again without force",
+ $modobj->module, $where ), $verbose );
+ return $where;
+ }
+
+ my ($remote_file, $local_file, $local_path);
+
+ ### build the local path to downlaod to ###
+ {
+ $local_path = $args->{fetchdir} ||
+ File::Spec->catdir(
+ $conf->get_conf('base'),
+ $modobj->path,
+ );
+
+ ### create the path if it doesn't exist ###
+ unless( -d $local_path ) {
+ unless( $self->_mkdir( dir => $local_path ) ) {
+ msg( loc("Could not create path '%1'", $local_path), $verbose);
+ return;
+ }
+ }
+
+ $local_file = File::Spec->rel2abs(
+ File::Spec->catfile(
+ $local_path,
+ $modobj->package,
+ )
+ );
+ }
+
+ ### do we already have the file? ###
+ if( -e $local_file ) {
+
+ if( $args->{force} ) {
+
+ ### some fetches will fail if the files exist already, so let's
+ ### delete them first
+ unlink $local_file
+ or msg( loc("Could not delete %1, some methods may " .
+ "fail to force a download", $local_file), $verbose);
+ } else {
+
+ ### store where we fetched it ###
+ $modobj->status->fetch( $local_file );
+
+ return $local_file;
+ }
+ }
+
+
+ ### we got a custom URI
+ if ( $fetch_from ) {
+ my $abs = $self->__file_fetch( from => $fetch_from,
+ to => $local_path,
+ verbose => $verbose );
+
+ unless( $abs ) {
+ error(loc("Unable to download '%1'", $fetch_from));
+ return;
+ }
+
+ ### store where we fetched it ###
+ $modobj->status->fetch( $abs );
+
+ return $abs;
+
+ ### we will get it from one of our mirrors
+ } else {
+ ### build the remote path to download from ###
+ { $remote_file = File::Spec::Unix->catfile(
+ $modobj->path,
+ $modobj->package,
+ );
+ unless( $remote_file ) {
+ error( loc('No remote file given for download') );
+ return;
+ }
+ }
+
+ ### see if we even have a host or a method to use to download with ###
+ my $found_host;
+ my @maybe_bad_host;
+
+ HOST: {
+ ### F*CKING PIECE OF F*CKING p4 SHIT makes
+ ### '$File :: Fetch::SOME_VAR'
+ ### into a meta variable and starts substituting the file name...
+ ### GRAAAAAAAAAAAAAAAAAAAAAAH!
+ ### use ' to combat it!
+
+ ### set up some flags for File::Fetch ###
+ local $File'Fetch::BLACKLIST = $conf->_get_fetch('blacklist');
+ local $File'Fetch::TIMEOUT = $conf->get_conf('timeout');
+ local $File'Fetch::DEBUG = $conf->get_conf('debug');
+ local $File'Fetch::FTP_PASSIVE = $conf->get_conf('passive');
+ local $File'Fetch::FROM_EMAIL = $conf->get_conf('email');
+ local $File'Fetch::PREFER_BIN = $conf->get_conf('prefer_bin');
+ local $File'Fetch::WARN = $verbose;
+
+
+ ### loop over all hosts we have ###
+ for my $host ( @{$conf->get_conf('hosts')} ) {
+ $found_host++;
+
+ my $mirror_path = File::Spec::Unix->catfile(
+ $host->{'path'}, $remote_file
+ );
+
+ ### build pretty print uri ###
+ my $where;
+ if( $host->{'scheme'} eq 'file' ) {
+ $where = CREATE_FILE_URI->(
+ File::Spec::Unix->rel2abs(
+ File::Spec::Unix->catdir(
+ grep { defined $_ && length $_ }
+ $host->{'host'},
+ $mirror_path
+ )
+ )
+ );
+ } else {
+ my %args = ( scheme => $host->{scheme},
+ host => $host->{host},
+ path => $mirror_path,
+ );
+
+ $where = $self->_host_to_uri( %args );
+ }
+
+ my $abs = $self->__file_fetch( from => $where,
+ to => $local_path,
+ verbose => $verbose );
+
+ ### we got a path back?
+ if( $abs ) {
+ ### store where we fetched it ###
+ $modobj->status->fetch( $abs );
+
+ ### this host is good, the previous ones are apparently
+ ### not, so mark them as such.
+ $self->_add_fail_host( host => $_ ) for @maybe_bad_host;
+
+ return $abs;
+ }
+
+ ### so we tried to get the file but didn't actually fetch it --
+ ### there's a chance this host is bad. mark it as such and
+ ### actually flag it back if we manage to get the file
+ ### somewhere else
+ push @maybe_bad_host, $host;
+ }
+ }
+
+ $found_host
+ ? error(loc("Fetch failed: host list exhausted " .
+ "-- are you connected today?"))
+ : error(loc("No hosts found to download from " .
+ "-- check your config"));
+ }
+
+ return;
+}
+
+sub __file_fetch {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my ($where, $local_path, $verbose);
+ my $tmpl = {
+ from => { required => 1, store => \$where },
+ to => { required => 1, store => \$local_path },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ msg(loc("Trying to get '%1'", $where ), $verbose );
+
+ ### build the object ###
+ my $ff = File::Fetch->new( uri => $where );
+
+ ### sanity check ###
+ error(loc("Bad uri '%1'",$where)), return unless $ff;
+
+ if( my $file = $ff->fetch( to => $local_path ) ) {
+ unless( -e $file && -s _ ) {
+ msg(loc("'%1' said it fetched '%2', but it was not created",
+ 'File::Fetch', $file), $verbose);
+
+ } else {
+ my $abs = File::Spec->rel2abs( $file );
+ return $abs;
+ }
+
+ } else {
+ error(loc("Fetching of '%1' failed: %2", $where, $ff->error));
+ }
+
+ return;
+}
+
+=pod
+
+=head2 _add_fail_host( host => $host_hashref )
+
+Mark a particular host as bad. This makes C<CPANPLUS::Internals::Fetch>
+skip it in fetches until this cache is flushed.
+
+=head2 _host_ok( host => $host_hashref )
+
+Query the cache to see if this host is ok, or if it has been flagged
+as bad.
+
+Returns true if the host is ok, false otherwise.
+
+=cut
+
+{ ### caching functions ###
+
+ sub _add_fail_host {
+ my $self = shift;
+ my %hash = @_;
+
+ my $host;
+ my $tmpl = {
+ host => { required => 1, default => {},
+ strict_type => 1, store => \$host },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ return $self->_hosts->{$host} = 1;
+ }
+
+ sub _host_ok {
+ my $self = shift;
+ my %hash = @_;
+
+ my $host;
+ my $tmpl = {
+ host => { required => 1, store => \$host },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ return $self->_hosts->{$host} ? 0 : 1;
+ }
+}
+
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Report.pm b/lib/CPANPLUS/Internals/Report.pm
new file mode 100644
index 0000000000..ffcb4f0d33
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Report.pm
@@ -0,0 +1,609 @@
+package CPANPLUS::Internals::Report;
+
+use strict;
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Internals::Constants::Report;
+
+use Data::Dumper;
+
+use Params::Check qw[check];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+use Module::Load::Conditional qw[can_load];
+
+$Params::Check::VERBOSE = 1;
+
+### for the version ###
+require CPANPLUS::Internals;
+
+=head1 NAME
+
+CPANPLUS::Internals::Report
+
+=head1 SYNOPSIS
+
+ ### enable test reporting
+ $cb->configure_object->set_conf( cpantest => 1 );
+
+ ### set custom mx host, shouldn't normally be needed
+ $cb->configure_object->set_conf( cpantest_mx => 'smtp.example.com' );
+
+=head1 DESCRIPTION
+
+This module provides all the functionality to send test reports to
+C<http://testers.cpan.org> using the C<Test::Reporter> module.
+
+All methods will be called automatically if you have C<CPANPLUS>
+configured to enable test reporting (see the C<SYNOPSIS>).
+
+=head1 METHODS
+
+=head2 $bool = $cb->_have_query_report_modules
+
+This function checks if all the required modules are here for querying
+reports. It returns true and loads them if they are, or returns false
+otherwise.
+
+=head2 $bool = $cb->_have_send_report_modules
+
+This function checks if all the required modules are here for sending
+reports. It returns true and loads them if they are, or returns false
+otherwise.
+
+=cut
+{ my $query_list = {
+ LWP => '0.0',
+ 'LWP::UserAgent' => '0.0',
+ 'HTTP::Request' => '0.0',
+ URI => '0.0',
+ YAML => '0.0',
+ };
+
+ my $send_list = {
+ %$query_list,
+ 'Test::Reporter' => 1.27,
+ };
+
+ sub _have_query_report_modules {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my $tmpl = {
+ verbose => { default => $conf->get_conf('verbose') },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ return can_load( modules => $query_list, verbose => $args->{verbose} )
+ ? 1
+ : 0;
+ }
+
+ sub _have_send_report_modules {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my $tmpl = {
+ verbose => { default => $conf->get_conf('verbose') },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ return can_load( modules => $send_list, verbose => $args->{verbose} )
+ ? 1
+ : 0;
+ }
+}
+
+=head2 @list = $cb->_query_report( module => $modobj, [all_versions => BOOL, verbose => BOOL] )
+
+This function queries the CPAN testers database at
+I<http://testers.cpan.org/> for test results of specified module objects,
+module names or distributions.
+
+The optional argument C<all_versions> controls whether all versions of
+a given distribution should be grabbed. It defaults to false
+(fetching only reports for the current version).
+
+Returns the a list with the following data structures (for CPANPLUS
+version 0.042) on success, or false on failure:
+
+ {
+ 'grade' => 'PASS',
+ 'dist' => 'CPANPLUS-0.042',
+ 'platform' => 'i686-pld-linux-thread-multi'
+ },
+ {
+ 'grade' => 'PASS',
+ 'dist' => 'CPANPLUS-0.042',
+ 'platform' => 'i686-linux-thread-multi'
+ },
+ {
+ 'grade' => 'FAIL',
+ 'dist' => 'CPANPLUS-0.042',
+ 'platform' => 'cygwin-multi-64int',
+ 'details' => 'http://nntp.x.perl.org/group/perl.cpan.testers/99371'
+ },
+ {
+ 'grade' => 'FAIL',
+ 'dist' => 'CPANPLUS-0.042',
+ 'platform' => 'i586-linux',
+ 'details' => 'http://nntp.x.perl.org/group/perl.cpan.testers/99396'
+ },
+
+The status of the test can be one of the following:
+UNKNOWN, PASS, FAIL or NA (not applicable).
+
+=cut
+
+sub _query_report {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my($mod, $verbose, $all);
+ my $tmpl = {
+ module => { required => 1, allow => IS_MODOBJ,
+ store => \$mod },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ all_versions => { default => 0, store => \$all },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### check if we have the modules we need for querying
+ return unless $self->_have_query_report_modules( verbose => 1 );
+
+ ### new user agent ###
+ my $ua = LWP::UserAgent->new;
+ $ua->agent( CPANPLUS_UA->() );
+
+ ### set proxies if we have them ###
+ $ua->env_proxy();
+
+ my $url = TESTERS_URL->($mod->package_name);
+ my $req = HTTP::Request->new( GET => $url);
+
+ msg( loc("Fetching: '%1'", $url), $verbose );
+
+ my $res = $ua->request( $req );
+
+ unless( $res->is_success ) {
+ error( loc( "Fetching report for '%1' failed: %2",
+ $url, $res->message ) );
+ return;
+ }
+
+ my $aref = YAML::Load( $res->content );
+
+ my $dist = $mod->package_name .'-'. $mod->package_version;
+
+ my @rv;
+ for my $href ( @$aref ) {
+ next unless $all or defined $href->{'distversion'} &&
+ $href->{'distversion'} eq $dist;
+
+ push @rv, { platform => $href->{'platform'},
+ grade => $href->{'action'},
+ dist => $href->{'distversion'},
+ ( $href->{'action'} eq 'FAIL'
+ ? (details => TESTERS_DETAILS_URL->($mod->package_name))
+ : ()
+ ) };
+ }
+
+ return @rv if @rv;
+ return;
+}
+
+=pod
+
+=head2 $bool = $cb->_send_report( module => $modobj, buffer => $make_output, failed => BOOL, [save => BOOL, address => $email_to, dontcc => BOOL, verbose => BOOL, force => BOOL]);
+
+This function sends a testers report to C<cpan-testers@perl.org> for a
+particular distribution.
+It returns true on success, and false on failure.
+
+It takes the following options:
+
+=over 4
+
+=item module
+
+The module object of this particular distribution
+
+=item buffer
+
+The output buffer from the 'make/make test' process
+
+=item failed
+
+Boolean indicating if the 'make/make test' went wrong
+
+=item save
+
+Boolean indicating if the report should be saved locally instead of
+mailed out. If provided, this function will return the location the
+report was saved to, rather than a simple boolean 'TRUE'.
+
+Defaults to false.
+
+=item address
+
+The email address to mail the report for. You should never need to
+override this, but it might be useful for debugging purposes.
+
+Defaults to C<cpan-testers@perl.org>.
+
+=item dontcc
+
+Boolean indicating whether or not we should Cc: the author. If false,
+previous error reports are inspected and checked if the author should
+be mailed. If set to true, these tests are skipped and the author is
+definitely not Cc:'d.
+You should probably not change this setting.
+
+Defaults to false.
+
+=item verbose
+
+Boolean indicating on whether or not to be verbose.
+
+Defaults to your configuration settings
+
+=item force
+
+Boolean indicating whether to force the sending, even if the max
+amount of reports for fails have already been reached, or if you
+may already have sent it before.
+
+Defaults to your configuration settings
+
+=back
+
+=cut
+
+sub _send_report {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ ### do you even /have/ test::reporter? ###
+ unless( $self->_have_send_report_modules(verbose => 1) ) {
+ error( loc( "You don't have '%1' (or modules required by '%2') ".
+ "installed, you cannot report test results.",
+ 'Test::Reporter', 'Test::Reporter' ) );
+ return;
+ }
+
+ ### check arguments ###
+ my ($buffer, $failed, $mod, $verbose, $force, $address, $save, $dontcc,
+ $tests_skipped );
+ my $tmpl = {
+ module => { required => 1, store => \$mod, allow => IS_MODOBJ },
+ buffer => { required => 1, store => \$buffer },
+ failed => { required => 1, store => \$failed },
+ address => { default => CPAN_TESTERS_EMAIL, store => \$address },
+ save => { default => 0, store => \$save },
+ dontcc => { default => 0, store => \$dontcc },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ force => { default => $conf->get_conf('force'),
+ store => \$force },
+ tests_skipped
+ => { default => 0, store => \$tests_skipped },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### get the data to fill the email with ###
+ my $name = $mod->module;
+ my $dist = $mod->package_name . '-' . $mod->package_version;
+ my $author = $mod->author->author;
+ my $email = $mod->author->email || CPAN_MAIL_ACCOUNT->( $author );
+ my $cp_conf = $conf->get_conf('cpantest') || '';
+ my $int_ver = $CPANPLUS::Internals::VERSION;
+ my $cb = $mod->parent;
+
+
+ ### determine the grade now ###
+
+ my $grade;
+ ### check if this is a platform specific module ###
+ ### if we failed the test, there may be reasons why
+ ### an 'NA' might have to be insted
+ GRADE: { if ( $failed ) {
+
+
+ ### XXX duplicated logic between this block
+ ### and REPORTED_LOADED_PREREQS :(
+
+ ### figure out if the prereqs are on CPAN at all
+ ### -- if not, send NA grade
+ ### Also, if our version of prereqs is too low,
+ ### -- send NA grade.
+ ### This is to address bug: #25327: do not count
+ ### as FAIL modules where prereqs are not filled
+ { my $prq = $mod->status->prereqs || {};
+
+ while( my($prq_name,$prq_ver) = each %$prq ) {
+ my $obj = $cb->module_tree( $prq_name );
+
+ unless( $obj ) {
+ msg(loc( "Prerequisite '%1' for '%2' could not be obtained".
+ " from CPAN -- sending N/A grade",
+ $prq_name, $name ), $verbose );
+
+ $grade = GRADE_NA;
+ last GRADE;
+ }
+
+ if( $cb->_vcmp( $prq_ver, $obj->installed_version ) > 0 ) {
+ msg(loc( "Installed version of '%1' ('%2') is too low for ".
+ "'%3' (needs '%4') -- sending N/A grade",
+ $prq_name, $obj->installed_version,
+ $name, $prq_ver ), $verbose );
+
+ $grade = GRADE_NA;
+ last GRADE;
+ }
+ }
+ }
+
+ unless( RELEVANT_TEST_RESULT->($mod) ) {
+ msg(loc(
+ "'%1' is a platform specific module, and the test results on".
+ " your platform are not relevant --sending N/A grade.",
+ $name), $verbose);
+
+ $grade = GRADE_NA;
+
+ } elsif ( UNSUPPORTED_OS->( $buffer ) ) {
+ msg(loc(
+ "'%1' is a platform specific module, and the test results on".
+ " your platform are not relevant --sending N/A grade.",
+ $name), $verbose);
+
+ $grade = GRADE_NA;
+
+ ### you dont have a high enough perl version?
+ } elsif ( PERL_VERSION_TOO_LOW->( $buffer ) ) {
+ msg(loc("'%1' requires a higher version of perl than your current ".
+ "version -- sending N/A grade.", $name), $verbose);
+
+ $grade = GRADE_NA;
+
+ ### perhaps where were no tests...
+ ### see if the thing even had tests ###
+ } elsif ( NO_TESTS_DEFINED->( $buffer ) ) {
+ $grade = GRADE_UNKNOWN;
+
+ } else {
+
+ $grade = GRADE_FAIL;
+ }
+
+ ### if we got here, it didn't fail and tests were present.. so a PASS
+ ### is in order
+ } else {
+ $grade = GRADE_PASS;
+ } }
+
+ ### so an error occurred, let's see what stage it went wrong in ###
+ my $message;
+ if( $grade eq GRADE_FAIL or $grade eq GRADE_UNKNOWN) {
+
+ ### return if one or more missing external libraries
+ if( my @missing = MISSING_EXTLIBS_LIST->($buffer) ) {
+ msg(loc("Not sending test report - " .
+ "external libraries not pre-installed"));
+ return 1;
+ }
+
+ ### will be 'fetch', 'make', 'test', 'install', etc ###
+ my $stage = TEST_FAIL_STAGE->($buffer);
+
+ ### return if we're only supposed to report make_test failures ###
+ return 1 if $cp_conf =~ /\bmaketest_only\b/i
+ and ($stage !~ /\btest\b/);
+
+ ### the header
+ $message = REPORT_MESSAGE_HEADER->( $int_ver, $author );
+
+ ### the bit where we inform what went wrong
+ $message .= REPORT_MESSAGE_FAIL_HEADER->( $stage, $buffer );
+
+ ### was it missing prereqs? ###
+ if( my @missing = MISSING_PREREQS_LIST->($buffer) ) {
+ if(!$self->_verify_missing_prereqs(
+ module => $mod,
+ missing => \@missing
+ )) {
+ msg(loc("Not sending test report - " .
+ "bogus missing prerequisites report"));
+ return 1;
+ }
+ $message .= REPORT_MISSING_PREREQS->($author,$email,@missing);
+ }
+
+ ### was it missing test files? ###
+ if( NO_TESTS_DEFINED->($buffer) ) {
+ $message .= REPORT_MISSING_TESTS->();
+ }
+
+ ### add a list of what modules have been loaded of your prereqs list
+ $message .= REPORT_LOADED_PREREQS->($mod);
+
+ ### the footer
+ $message .= REPORT_MESSAGE_FOOTER->();
+
+ ### it may be another grade than fail/unknown.. may be worth noting
+ ### that tests got skipped, since the buffer is not added in
+ } elsif ( $tests_skipped ) {
+ $message .= REPORT_TESTS_SKIPPED->();
+ }
+
+ ### if it failed, and that already got reported, we're not cc'ing the
+ ### author. Also, 'dont_cc' might be in the config, so check this;
+ my $dont_cc_author = $dontcc;
+
+ unless( $dont_cc_author ) {
+ if( $cp_conf =~ /\bdont_cc\b/i ) {
+ $dont_cc_author++;
+
+ } elsif ( $grade eq GRADE_PASS ) {
+ $dont_cc_author++
+
+ } elsif( $grade eq GRADE_FAIL ) {
+ my @already_sent =
+ $self->_query_report( module => $mod, verbose => $verbose );
+
+ ### if we can't fetch it, we'll just assume no one
+ ### mailed him yet
+ my $count = 0;
+ if( @already_sent ) {
+ for my $href (@already_sent) {
+ $count++ if uc $href->{'grade'} eq uc GRADE_FAIL;
+ }
+ }
+
+ if( $count > MAX_REPORT_SEND and !$force) {
+ msg(loc("'%1' already reported for '%2', ".
+ "not cc-ing the author",
+ GRADE_FAIL, $dist ), $verbose );
+ $dont_cc_author++;
+ }
+ }
+ }
+
+ ### reporter object ###
+ my $reporter = Test::Reporter->new(
+ grade => $grade,
+ distribution => $dist,
+ via => "CPANPLUS $int_ver",
+ debug => $conf->get_conf('debug'),
+ );
+
+ ### set a custom mx, if requested
+ $reporter->mx( [ $conf->get_conf('cpantest_mx') ] )
+ if $conf->get_conf('cpantest_mx');
+
+ ### set the from address ###
+ $reporter->from( $conf->get_conf('email') )
+ if $conf->get_conf('email') !~ /\@example\.\w+$/i;
+
+ ### give the user a chance to programattically alter the message
+ $message = $self->_callbacks->munge_test_report->($mod, $message, $grade);
+
+ ### add the body if we have any ###
+ $reporter->comments( $message ) if defined $message && length $message;
+
+ ### do a callback to ask if we should send the report
+ unless ($self->_callbacks->send_test_report->($mod, $grade)) {
+ msg(loc("Ok, not sending test report"));
+ return 1;
+ }
+
+ ### do a callback to ask if we should edit the report
+ if ($self->_callbacks->edit_test_report->($mod, $grade)) {
+ ### test::reporter 1.20 and lower don't have a way to set
+ ### the preferred editor with a method call, but it does
+ ### respect your env variable, so let's set that.
+ local $ENV{VISUAL} = $conf->get_program('editor')
+ if $conf->get_program('editor');
+
+ $reporter->edit_comments;
+ }
+
+ ### people to mail ###
+ my @inform;
+ #push @inform, $email unless $dont_cc_author;
+
+ ### allow to be overridden, but default to the normal address ###
+ $reporter->address( $address );
+
+ ### should we save it locally? ###
+ if( $save ) {
+ if( my $file = $reporter->write() ) {
+ msg(loc("Successfully wrote report for '%1' to '%2'",
+ $dist, $file), $verbose);
+ return $file;
+
+ } else {
+ error(loc("Failed to write report for '%1'", $dist));
+ return;
+ }
+
+ ### should we send it to a bunch of people? ###
+ ### XXX should we do an 'already sent' check? ###
+ } elsif( $reporter->send( @inform ) ) {
+ msg(loc("Successfully sent '%1' report for '%2'", $grade, $dist),
+ $verbose);
+ return 1;
+
+ ### something broke :( ###
+ } else {
+ error(loc("Could not send '%1' report for '%2': %3",
+ $grade, $dist, $reporter->errstr));
+ return;
+ }
+}
+
+sub _verify_missing_prereqs {
+ my $self = shift;
+ my %hash = @_;
+
+ ### check arguments ###
+ my ($mod, $missing);
+ my $tmpl = {
+ module => { required => 1, store => \$mod },
+ missing => { required => 1, store => \$missing },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+
+ my %missing = map {$_ => 1} @$missing;
+ my $conf = $self->configure_object;
+ my $extract = $mod->status->extract;
+
+ ### Read pre-requisites from Makefile.PL or Build.PL (if there is one),
+ ### of the form:
+ ### 'PREREQ_PM' => {
+ ### 'Compress::Zlib' => '1.20',
+ ### 'Test::More' => 0,
+ ### },
+ ### Build.PL uses 'requires' instead of 'PREREQ_PM'.
+
+ my @search;
+ push @search, ($extract ? MAKEFILE_PL->( $extract ) : MAKEFILE_PL->());
+ push @search, ($extract ? BUILD_PL->( $extract ) : BUILD_PL->());
+
+ for my $file ( @search ) {
+ if(-e $file and -r $file) {
+ my $slurp = $self->_get_file_contents(file => $file);
+ my ($prereq) =
+ ($slurp =~ /'?(?:PREREQ_PM|requires)'?\s*=>\s*{(.*?)}/s);
+ my @prereq =
+ ($prereq =~ /'?([\w\:]+)'?\s*=>\s*'?\d[\d\.\-\_]*'?/sg);
+ delete $missing{$_} for(@prereq);
+ }
+ }
+
+ return 1 if(keys %missing); # There ARE missing prerequisites
+ return; # All prerequisites accounted for
+}
+
+1;
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Search.pm b/lib/CPANPLUS/Internals/Search.pm
new file mode 100644
index 0000000000..30443f09fd
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Search.pm
@@ -0,0 +1,316 @@
+package CPANPLUS::Internals::Search;
+
+use strict;
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Module;
+use CPANPLUS::Module::Author;
+
+use File::Find;
+use File::Spec;
+
+use Params::Check qw[check allow];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+$Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Internals::Search
+
+=head1 SYNOPSIS
+
+ my $aref = $cpan->_search_module_tree(
+ type => 'package',
+ allow => [qr/DBI/],
+ );
+
+ my $aref = $cpan->_search_author_tree(
+ type => 'cpanid',
+ data => \@old_results,
+ verbose => 1,
+ allow => [qw|KANE AUTRIJUS|],
+ );
+
+ my $aref = $cpan->_all_installed( );
+
+=head1 DESCRIPTION
+
+The functions in this module are designed to find module(objects)
+based on certain criteria and return them.
+
+=head1 METHODS
+
+=head2 _search_module_tree( type => TYPE, allow => \@regexex, [data => \@previous_results ] )
+
+Searches the moduletree for module objects matching the criteria you
+specify. Returns an array ref of module objects on success, and false
+on failure.
+
+It takes the following arguments:
+
+=over 4
+
+=item type
+
+This can be any of the accessors for the C<CPANPLUS::Module> objects.
+This is a required argument.
+
+=item allow
+
+A set of rules, or more precisely, a list of regexes (via C<qr//> or
+plain strings), that the C<type> must adhere too. You can specify as
+many as you like, and it will be treated as an C<OR> search.
+For an C<AND> search, see the C<data> argument.
+
+This is a required argument.
+
+=item data
+
+An arrayref of previous search results. This is the way to do an C<AND>
+search -- C<_search_module_tree> will only search the module objects
+specified in C<data> if provided, rather than the moduletree itself.
+
+=back
+
+=cut
+
+# Although the Params::Check solution is more graceful, it is WAY too slow.
+#
+# This sample script:
+#
+# use CPANPLUS::Backend;
+# my $cb = new CPANPLUS::Backend;
+# $cb->module_tree;
+# my @list = $cb->search( type => 'module', allow => [qr/^Acme/] );
+# print $_->module, $/ for @list;
+#
+# Produced the following output using Dprof WITH params::check code
+#
+# Total Elapsed Time = 3.670024 Seconds
+# User+System Time = 3.390373 Seconds
+# Exclusive Times
+# %Time ExclSec CumulS #Calls sec/call Csec/c Name
+# 88.7 3.008 4.463 20610 0.0001 0.0002 Params::Check::check
+# 47.4 1.610 1.610 1 1.6100 1.6100 Storable::net_pstore
+# 25.6 0.869 0.737 20491 0.0000 0.0000 Locale::Maketext::Simple::_default
+# _gettext
+# 23.2 0.789 0.524 40976 0.0000 0.0000 Params::Check::_who_was_it
+# 23.2 0.789 0.677 20610 0.0000 0.0000 Params::Check::_sanity_check
+# 19.7 0.670 0.670 1 0.6700 0.6700 Storable::pretrieve
+# 14.1 0.480 0.211 41350 0.0000 0.0000 Params::Check::_convert_case
+# 11.5 0.390 0.256 20610 0.0000 0.0000 Params::Check::_hashdefs
+# 11.5 0.390 0.255 20697 0.0000 0.0000 Params::Check::_listreqs
+# 11.4 0.389 0.177 20653 0.0000 0.0000 Params::Check::_canon_key
+# 10.9 0.370 0.356 20697 0.0000 0.0000 Params::Check::_hasreq
+# 8.02 0.272 4.750 1 0.2723 4.7501 CPANPLUS::Internals::Search::_sear
+# ch_module_tree
+# 6.49 0.220 0.086 20653 0.0000 0.0000 Params::Check::_iskey
+# 6.19 0.210 0.077 20488 0.0000 0.0000 Params::Check::_store_error
+# 5.01 0.170 0.036 20680 0.0000 0.0000 CPANPLUS::Module::__ANON__
+#
+# and this output /without/
+#
+# Total Elapsed Time = 2.803426 Seconds
+# User+System Time = 2.493426 Seconds
+# Exclusive Times
+# %Time ExclSec CumulS #Calls sec/call Csec/c Name
+# 56.9 1.420 1.420 1 1.4200 1.4200 Storable::net_pstore
+# 25.6 0.640 0.640 1 0.6400 0.6400 Storable::pretrieve
+# 9.22 0.230 0.096 20680 0.0000 0.0000 CPANPLUS::Module::__ANON__
+# 7.06 0.176 0.272 1 0.1762 0.2719 CPANPLUS::Internals::Search::_sear
+# ch_module_tree
+# 3.21 0.080 0.098 10 0.0080 0.0098 IPC::Cmd::BEGIN
+# 1.60 0.040 0.205 13 0.0031 0.0158 CPANPLUS::Internals::BEGIN
+# 1.20 0.030 0.030 29 0.0010 0.0010 vars::BEGIN
+# 1.20 0.030 0.117 10 0.0030 0.0117 Log::Message::BEGIN
+# 1.20 0.030 0.029 9 0.0033 0.0033 CPANPLUS::Internals::Search::BEGIN
+# 0.80 0.020 0.020 5 0.0040 0.0040 DynaLoader::dl_load_file
+# 0.80 0.020 0.127 10 0.0020 0.0127 CPANPLUS::Module::BEGIN
+# 0.80 0.020 0.389 2 0.0099 0.1944 main::BEGIN
+# 0.80 0.020 0.359 12 0.0017 0.0299 CPANPLUS::Backend::BEGIN
+# 0.40 0.010 0.010 30 0.0003 0.0003 Config::FETCH
+# 0.40 0.010 0.010 18 0.0006 0.0005 Locale::Maketext::Simple::load_loc
+#
+
+sub _search_module_tree {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my($mods,$list,$verbose,$type);
+ my $tmpl = {
+ data => { default => [values %{$self->module_tree}],
+ strict_type=> 1, store => \$mods },
+ allow => { required => 1, default => [ ], strict_type => 1,
+ store => \$list },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ type => { required => 1, allow => [CPANPLUS::Module->accessors()],
+ store => \$type },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ { local $Params::Check::VERBOSE = 0;
+
+ my @rv;
+ for my $mod (@$mods) {
+ #push @rv, $mod if check(
+ # { $type => { allow => $list } },
+ # { $type => $mod->$type() }
+ # );
+ push @rv, $mod if allow( $mod->$type() => $list );
+
+ }
+ return \@rv;
+ }
+}
+
+=pod
+
+=head2 _search_author_tree( type => TYPE, allow => \@regexex, [data => \@previous_results ] )
+
+Searches the authortree for author objects matching the criteria you
+specify. Returns an array ref of author objects on success, and false
+on failure.
+
+It takes the following arguments:
+
+=over 4
+
+=item type
+
+This can be any of the accessors for the C<CPANPLUS::Module::Author>
+objects. This is a required argument.
+
+=item allow
+
+
+A set of rules, or more precisely, a list of regexes (via C<qr//> or
+plain strings), that the C<type> must adhere too. You can specify as
+many as you like, and it will be treated as an C<OR> search.
+For an C<AND> search, see the C<data> argument.
+
+This is a required argument.
+
+=item data
+
+An arrayref of previous search results. This is the way to do an C<and>
+search -- C<_search_author_tree> will only search the author objects
+specified in C<data> if provided, rather than the authortree itself.
+
+=back
+
+=cut
+
+sub _search_author_tree {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my($authors,$list,$verbose,$type);
+ my $tmpl = {
+ data => { default => [values %{$self->author_tree}],
+ strict_type=> 1, store => \$authors },
+ allow => { required => 1, default => [ ], strict_type => 1,
+ store => \$list },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ type => { required => 1, allow => [CPANPLUS::Module::Author->accessors()],
+ store => \$type },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ { local $Params::Check::VERBOSE = 0;
+
+ my @rv;
+ for my $auth (@$authors) {
+ #push @rv, $auth if check(
+ # { $type => { allow => $list } },
+ # { $type => $auth->$type }
+ # );
+ push @rv, $auth if allow( $auth->$type() => $list );
+ }
+ return \@rv;
+ }
+
+
+}
+
+=pod
+
+=head2 _all_installed()
+
+This function returns an array ref of module objects of modules that
+are installed on this system.
+
+=cut
+
+sub _all_installed {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my %hash = @_;
+
+ my %seen; my @rv;
+
+
+ ### File::Find uses lstat, which quietly becomes stat on win32
+ ### it then uses -l _ which is not allowed by the statbuffer because
+ ### you did a stat, not an lstat (duh!). so don't tell win32 to
+ ### follow symlinks, as that will break badly
+ my %find_args = ();
+ $find_args{'follow_fast'} = 1 unless $^O eq 'MSWin32';
+
+ ### never use the @INC hooks to find installed versions of
+ ### modules -- they're just there in case they're not on the
+ ### perl install, but the user shouldn't trust them for *other*
+ ### modules!
+ ### XXX CPANPLUS::inc is now obsolete, remove the calls
+ #local @INC = CPANPLUS::inc->original_inc;
+
+ for my $dir (@INC ) {
+ next if $dir eq '.';
+
+ ### not a directory after all ###
+ next unless -d $dir;
+
+ ### make sure to clean up the directories just in case,
+ ### as we're making assumptions about the length
+ ### This solves rt.cpan issue #19738
+ $dir = File::Spec->canonpath( $dir );
+
+ File::Find::find(
+ { %find_args,
+ wanted => sub {
+
+ return unless /\.pm$/i;
+ my $mod = $File::Find::name;
+
+ $mod = substr($mod, length($dir) + 1, -3);
+ $mod = join '::', File::Spec->splitdir($mod);
+
+ return if $seen{$mod}++;
+ my $modobj = $self->module_tree($mod) or return;
+
+ push @rv, $modobj;
+ },
+ }, $dir
+ );
+ }
+
+ return \@rv;
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Source.pm b/lib/CPANPLUS/Internals/Source.pm
new file mode 100644
index 0000000000..c58632b355
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Source.pm
@@ -0,0 +1,1011 @@
+package CPANPLUS::Internals::Source;
+
+use strict;
+
+use CPANPLUS::Error;
+use CPANPLUS::Module;
+use CPANPLUS::Module::Fake;
+use CPANPLUS::Module::Author;
+use CPANPLUS::Internals::Constants;
+
+use Archive::Extract;
+
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+use Params::Check qw[check];
+use IPC::Cmd qw[can_run];
+use Module::Load::Conditional qw[can_load];
+
+$Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Internals::Source
+
+=head1 SYNOPSIS
+
+ ### lazy load author/module trees ###
+
+ $cb->_author_tree;
+ $cb->_module_tree;
+
+=head1 DESCRIPTION
+
+CPANPLUS::Internals::Source controls the updating of source files and
+the parsing of them into usable module/author trees to be used by
+C<CPANPLUS>.
+
+Functions exist to check if source files are still C<good to use> as
+well as update them, and then parse them.
+
+The flow looks like this:
+
+ $cb->_author_tree || $cb->_module_tree
+ $cb->__check_trees
+ $cb->__check_uptodate
+ $cb->_update_source
+ $cb->_build_trees
+ $cb->__create_author_tree
+ $cb->__retrieve_source
+ $cb->__create_module_tree
+ $cb->__retrieve_source
+ $cb->__create_dslip_tree
+ $cb->__retrieve_source
+ $cb->_save_source
+
+ $cb->_dslip_defs
+
+=head1 METHODS
+
+=cut
+
+{
+ my $recurse; # flag to prevent recursive calls to *_tree functions
+
+ ### lazy loading of module tree
+ sub _module_tree {
+ my $self = $_[0];
+
+ unless ($self->{_modtree} or $recurse++ > 0) {
+ my $uptodate = $self->_check_trees( @_[1..$#_] );
+ $self->_build_trees(uptodate => $uptodate);
+ }
+
+ $recurse--;
+ return $self->{_modtree};
+ }
+
+ ### lazy loading of author tree
+ sub _author_tree {
+ my $self = $_[0];
+
+ unless ($self->{_authortree} or $recurse++ > 0) {
+ my $uptodate = $self->_check_trees( @_[1..$#_] );
+ $self->_build_trees(uptodate => $uptodate);
+ }
+
+ $recurse--;
+ return $self->{_authortree};
+ }
+
+}
+
+=pod
+
+=head2 $cb->_check_trees( [update_source => BOOL, path => PATH, verbose => BOOL] )
+
+Retrieve source files and return a boolean indicating whether or not
+the source files are up to date.
+
+Takes several arguments:
+
+=over 4
+
+=item update_source
+
+A flag to force re-fetching of the source files, even
+if they are still up to date.
+
+=item path
+
+The absolute path to the directory holding the source files.
+
+=item verbose
+
+A boolean flag indicating whether or not to be verbose.
+
+=back
+
+Will get information from the config file by default.
+
+=cut
+
+### retrieve source files, and returns a boolean indicating if it's up to date
+sub _check_trees {
+ my ($self, %hash) = @_;
+ my $conf = $self->configure_object;
+
+ my $update_source;
+ my $verbose;
+ my $path;
+
+ my $tmpl = {
+ path => { default => $conf->get_conf('base'),
+ store => \$path
+ },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose
+ },
+ update_source => { default => 0, store => \$update_source },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### if the user never wants to update their source without explicitly
+ ### telling us, shortcircuit here
+ return 1 if $conf->get_conf('no_update') && !$update_source;
+
+ ### a check to see if our source files are still up to date ###
+ msg( loc("Checking if source files are up to date"), $verbose );
+
+ my $uptodate = 1; # default return value
+
+ for my $name (qw[auth dslip mod]) {
+ for my $file ( $conf->_get_source( $name ) ) {
+ $self->__check_uptodate(
+ file => File::Spec->catfile( $args->{path}, $file ),
+ name => $name,
+ update_source => $update_source,
+ verbose => $verbose,
+ ) or $uptodate = 0;
+ }
+ }
+
+ return $uptodate;
+}
+
+=pod
+
+=head2 $cb->__check_uptodate( file => $file, name => $name, [update_source => BOOL, verbose => BOOL] )
+
+C<__check_uptodate> checks if a given source file is still up-to-date
+and if not, or when C<update_source> is true, will re-fetch the source
+file.
+
+Takes the following arguments:
+
+=over 4
+
+=item file
+
+The source file to check.
+
+=item name
+
+The internal shortcut name for the source file (used for config
+lookups).
+
+=item update_source
+
+Flag to force updating of sourcefiles regardless.
+
+=item verbose
+
+Boolean to indicate whether to be verbose or not.
+
+=back
+
+Returns a boolean value indicating whether the current files are up
+to date or not.
+
+=cut
+
+### this method checks whether or not the source files we are using are still up to date
+sub __check_uptodate {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+
+ my $tmpl = {
+ file => { required => 1 },
+ name => { required => 1 },
+ update_source => { default => 0 },
+ verbose => { default => $conf->get_conf('verbose') },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ my $flag;
+ unless ( -e $args->{'file'} && (
+ ( stat $args->{'file'} )[9]
+ + $conf->_get_source('update') )
+ > time ) {
+ $flag = 1;
+ }
+
+ if ( $flag or $args->{'update_source'} ) {
+
+ if ( $self->_update_source( name => $args->{'name'} ) ) {
+ return 0; # return 0 so 'uptodate' will be set to 0, meaning no use
+ # of previously stored hashrefs!
+ } else {
+ msg( loc("Unable to update source, attempting to get away with using old source file!"), $args->{verbose} );
+ return 1;
+ }
+
+ } else {
+ return 1;
+ }
+}
+
+=pod
+
+=head2 $cb->_update_source( name => $name, [path => $path, verbose => BOOL] )
+
+This method does the actual fetching of source files.
+
+It takes the following arguments:
+
+=over 4
+
+=item name
+
+The internal shortcut name for the source file (used for config
+lookups).
+
+=item path
+
+The full path where to write the files.
+
+=item verbose
+
+Boolean to indicate whether to be verbose or not.
+
+=back
+
+Returns a boolean to indicate success.
+
+=cut
+
+### this sub fetches new source files ###
+sub _update_source {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+
+ my $tmpl = {
+ name => { required => 1 },
+ path => { default => $conf->get_conf('base') },
+ verbose => { default => $conf->get_conf('verbose') },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+
+ my $path = $args->{path};
+ my $now = time;
+
+ { ### this could use a clean up - Kane
+ ### no worries about the / -> we get it from the _ftp configuration, so
+ ### it's not platform dependant. -kane
+ my ($dir, $file) = $conf->_get_mirror( $args->{'name'} ) =~ m|(.+/)(.+)$|sg;
+
+ msg( loc("Updating source file '%1'", $file), $args->{'verbose'} );
+
+ my $fake = CPANPLUS::Module::Fake->new(
+ module => $args->{'name'},
+ path => $dir,
+ package => $file,
+ _id => $self->_id,
+ );
+
+ ### can't use $fake->fetch here, since ->parent won't work --
+ ### the sources haven't been saved yet
+ my $rv = $self->_fetch(
+ module => $fake,
+ fetchdir => $path,
+ force => 1,
+ );
+
+
+ unless ($rv) {
+ error( loc("Couldn't fetch '%1'", $file) );
+ return;
+ }
+
+ ### `touch` the file, so windoze knows it's new -jmb
+ ### works on *nix too, good fix -Kane
+ utime ( $now, $now, File::Spec->catfile($path, $file) ) or
+ error( loc("Couldn't touch %1", $file) );
+
+ }
+ return 1;
+}
+
+=pod
+
+=head2 $cb->_build_trees( uptodate => BOOL, [use_stored => BOOL, path => $path, verbose => BOOL] )
+
+This method rebuilds the author- and module-trees from source.
+
+It takes the following arguments:
+
+=over 4
+
+=item uptodate
+
+Indicates whether any on disk caches are still ok to use.
+
+=item path
+
+The absolute path to the directory holding the source files.
+
+=item verbose
+
+A boolean flag indicating whether or not to be verbose.
+
+=item use_stored
+
+A boolean flag indicating whether or not it is ok to use previously
+stored trees. Defaults to true.
+
+=back
+
+Returns a boolean indicating success.
+
+=cut
+
+### (re)build the trees ###
+sub _build_trees {
+ my ($self, %hash) = @_;
+ my $conf = $self->configure_object;
+
+ my($path,$uptodate,$use_stored);
+ my $tmpl = {
+ path => { default => $conf->get_conf('base'), store => \$path },
+ verbose => { default => $conf->get_conf('verbose') },
+ uptodate => { required => 1, store => \$uptodate },
+ use_stored => { default => 1, store => \$use_stored },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return undef;
+
+ ### retrieve the stored source files ###
+ my $stored = $self->__retrieve_source(
+ path => $path,
+ uptodate => $uptodate && $use_stored,
+ verbose => $args->{'verbose'},
+ ) || {};
+
+ ### build the trees ###
+ $self->{_authortree} = $stored->{_authortree} ||
+ $self->__create_author_tree(
+ uptodate => $uptodate,
+ path => $path,
+ verbose => $args->{verbose},
+ );
+ $self->{_modtree} = $stored->{_modtree} ||
+ $self->_create_mod_tree(
+ uptodate => $uptodate,
+ path => $path,
+ verbose => $args->{verbose},
+ );
+
+ ### return if we weren't able to build the trees ###
+ return unless $self->{_modtree} && $self->{_authortree};
+
+ ### write the stored files to disk, so we can keep using them
+ ### from now on, till they become invalid
+ ### write them if the original sources weren't uptodate, or
+ ### we didn't just load storable files
+ $self->_save_source() if !$uptodate or not keys %$stored;
+
+ ### still necessary? can only run one instance now ###
+ ### will probably stay that way --kane
+# my $id = $self->_store_id( $self );
+#
+# unless ( $id == $self->_id ) {
+# error( loc("IDs do not match: %1 != %2. Storage failed!", $id, $self->_id) );
+# }
+
+ return 1;
+}
+
+=pod
+
+=head2 $cb->__retrieve_source(name => $name, [path => $path, uptodate => BOOL, verbose => BOOL])
+
+This method retrieves a I<storable>d tree identified by C<$name>.
+
+It takes the following arguments:
+
+=over 4
+
+=item name
+
+The internal name for the source file to retrieve.
+
+=item uptodate
+
+A flag indicating whether the file-cache is up-to-date or not.
+
+=item path
+
+The absolute path to the directory holding the source files.
+
+=item verbose
+
+A boolean flag indicating whether or not to be verbose.
+
+=back
+
+Will get information from the config file by default.
+
+Returns a tree on success, false on failure.
+
+=cut
+
+sub __retrieve_source {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+ my $tmpl = {
+ path => { default => $conf->get_conf('base') },
+ verbose => { default => $conf->get_conf('verbose') },
+ uptodate => { default => 0 },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### check if we can retrieve a frozen data structure with storable ###
+ my $storable = can_load( modules => {'Storable' => '0.0'} )
+ if $conf->get_conf('storable');
+
+ return unless $storable;
+
+ ### $stored is the name of the frozen data structure ###
+ my $stored = $self->__storable_file( $args->{path} );
+
+ if ($storable && -e $stored && -s _ && $args->{'uptodate'}) {
+ msg( loc("Retrieving %1", $stored), $args->{'verbose'} );
+
+ my $href = Storable::retrieve($stored);
+ return $href;
+ } else {
+ return;
+ }
+}
+
+=pod
+
+=head2 $cb->_save_source([verbose => BOOL, path => $path])
+
+This method saves all the parsed trees in I<storable>d format if
+C<Storable> is available.
+
+It takes the following arguments:
+
+=over 4
+
+=item path
+
+The absolute path to the directory holding the source files.
+
+=item verbose
+
+A boolean flag indicating whether or not to be verbose.
+
+=back
+
+Will get information from the config file by default.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _save_source {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+
+ my $tmpl = {
+ path => { default => $conf->get_conf('base'), allow => DIR_EXISTS },
+ verbose => { default => $conf->get_conf('verbose') },
+ force => { default => 1 },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ my $aref = [qw[_modtree _authortree]];
+
+ ### check if we can retrieve a frozen data structure with storable ###
+ my $storable;
+ $storable = can_load( modules => {'Storable' => '0.0'} )
+ if $conf->get_conf('storable');
+ return unless $storable;
+
+ my $to_write = {};
+ foreach my $key ( @$aref ) {
+ next unless ref( $self->{$key} );
+ $to_write->{$key} = $self->{$key};
+ }
+
+ return unless keys %$to_write;
+
+ ### $stored is the name of the frozen data structure ###
+ my $stored = $self->__storable_file( $args->{path} );
+
+ if (-e $stored && not -w $stored) {
+ msg( loc("%1 not writable; skipped.", $stored), $args->{'verbose'} );
+ return;
+ }
+
+ msg( loc("Writing compiled source information to disk. This might take a little while."),
+ $args->{'verbose'} );
+
+ my $flag;
+ unless( Storable::nstore( $to_write, $stored ) ) {
+ error( loc("could not store %1!", $stored) );
+ $flag++;
+ }
+
+ return $flag ? 0 : 1;
+}
+
+sub __storable_file {
+ my $self = shift;
+ my $conf = $self->configure_object;
+ my $path = shift or return;
+
+ ### check if we can retrieve a frozen data structure with storable ###
+ my $storable = $conf->get_conf('storable')
+ ? can_load( modules => {'Storable' => '0.0'} )
+ : 0;
+
+ return unless $storable;
+
+ ### $stored is the name of the frozen data structure ###
+ ### changed to use File::Spec->catfile -jmb
+ my $stored = File::Spec->rel2abs(
+ File::Spec->catfile(
+ $path, #base dir
+ $conf->_get_source('stored') #file
+ . '.' .
+ $Storable::VERSION #the version of storable
+ . '.stored' #append a suffix
+ )
+ );
+
+ return $stored;
+}
+
+=pod
+
+=head2 $cb->__create_author_tree([path => $path, uptodate => BOOL, verbose => BOOL])
+
+This method opens a source files and parses its contents into a
+searchable author-tree or restores a file-cached version of a
+previous parse, if the sources are uptodate and the file-cache exists.
+
+It takes the following arguments:
+
+=over 4
+
+=item uptodate
+
+A flag indicating whether the file-cache is uptodate or not.
+
+=item path
+
+The absolute path to the directory holding the source files.
+
+=item verbose
+
+A boolean flag indicating whether or not to be verbose.
+
+=back
+
+Will get information from the config file by default.
+
+Returns a tree on success, false on failure.
+
+=cut
+
+sub __create_author_tree() {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+
+ my $tmpl = {
+ path => { default => $conf->get_conf('base') },
+ verbose => { default => $conf->get_conf('verbose') },
+ uptodate => { default => 0 },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+ my $tree = {};
+ my $file = File::Spec->catfile(
+ $args->{path},
+ $conf->_get_source('auth')
+ );
+
+ msg(loc("Rebuilding author tree, this might take a while"),
+ $args->{verbose});
+
+ ### extract the file ###
+ my $ae = Archive::Extract->new( archive => $file ) or return;
+ my $out = STRIP_GZ_SUFFIX->($file);
+
+ ### make sure to set the PREFER_BIN flag if desired ###
+ { local $Archive::Extract::PREFER_BIN = $conf->get_conf('prefer_bin');
+ $ae->extract( to => $out ) or return;
+ }
+
+ my $cont = $self->_get_file_contents( file => $out ) or return;
+
+ ### don't need it anymore ###
+ unlink $out;
+
+ for ( split /\n/, $cont ) {
+ my($id, $name, $email) = m/^alias \s+
+ (\S+) \s+
+ "\s* ([^\"\<]+?) \s* <(.+)> \s*"
+ /x;
+
+ $tree->{$id} = CPANPLUS::Module::Author->new(
+ author => $name, #authors name
+ email => $email, #authors email address
+ cpanid => $id, #authors CPAN ID
+ _id => $self->_id, #id of this internals object
+ );
+ }
+
+ return $tree;
+
+} #__create_author_tree
+
+=pod
+
+=head2 $cb->_create_mod_tree([path => $path, uptodate => BOOL, verbose => BOOL])
+
+This method opens a source files and parses its contents into a
+searchable module-tree or restores a file-cached version of a
+previous parse, if the sources are uptodate and the file-cache exists.
+
+It takes the following arguments:
+
+=over 4
+
+=item uptodate
+
+A flag indicating whether the file-cache is up-to-date or not.
+
+=item path
+
+The absolute path to the directory holding the source files.
+
+=item verbose
+
+A boolean flag indicating whether or not to be verbose.
+
+=back
+
+Will get information from the config file by default.
+
+Returns a tree on success, false on failure.
+
+=cut
+
+### this builds a hash reference with the structure of the cpan module tree ###
+sub _create_mod_tree {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+
+ my $tmpl = {
+ path => { default => $conf->get_conf('base') },
+ verbose => { default => $conf->get_conf('verbose') },
+ uptodate => { default => 0 },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return undef;
+ my $file = File::Spec->catfile($args->{path}, $conf->_get_source('mod'));
+
+ msg(loc("Rebuilding module tree, this might take a while"),
+ $args->{verbose});
+
+
+ my $dslip_tree = $self->__create_dslip_tree( %$args );
+
+ ### extract the file ###
+ my $ae = Archive::Extract->new( archive => $file ) or return;
+ my $out = STRIP_GZ_SUFFIX->($file);
+
+ ### make sure to set the PREFER_BIN flag if desired ###
+ { local $Archive::Extract::PREFER_BIN = $conf->get_conf('prefer_bin');
+ $ae->extract( to => $out ) or return;
+ }
+
+ my $cont = $self->_get_file_contents( file => $out ) or return;
+
+ ### don't need it anymore ###
+ unlink $out;
+
+ my $tree = {};
+ my $flag;
+
+ for ( split /\n/, $cont ) {
+
+ ### quick hack to read past the header of the file ###
+ ### this is still rather evil... fix some time - Kane
+ $flag = 1 if m|^\s*$|;
+ next unless $flag;
+
+ ### skip empty lines ###
+ next unless /\S/;
+ chomp;
+
+ my @data = split /\s+/;
+
+ ### filter out the author and filename as well ###
+ ### authors can apparently have digits in their names,
+ ### and dirs can have dots... blah!
+ my ($author, $package) = $data[2] =~
+ m| [A-Z\d-]/
+ [A-Z\d-]{2}/
+ ([A-Z\d-]+) (?:/[\S]+)?/
+ ([^/]+)$
+ |xsg;
+
+ ### remove file name from the path
+ $data[2] =~ s|/[^/]+$||;
+
+
+ unless( $self->author_tree($author) ) {
+ error( loc( "No such author '%1' -- can't make module object " .
+ "'%2' that is supposed to belong to this author",
+ $author, $data[0] ) );
+ next;
+ }
+
+ ### adding the dslip info
+ ### probably can use some optimization
+ my $dslip;
+ for my $item ( qw[ statd stats statl stati statp ] ) {
+ ### checking if there's an entry in the dslip info before
+ ### catting it on. appeasing warnings this way
+ $dslip .= $dslip_tree->{ $data[0] }->{$item}
+ ? $dslip_tree->{ $data[0] }->{$item}
+ : ' ';
+ }
+
+ ### Every module get's stored as a module object ###
+ $tree->{ $data[0] } = CPANPLUS::Module->new(
+ module => $data[0], # full module name
+ version => ($data[1] eq 'undef' # version number
+ ? '0.0'
+ : $data[1]),
+ path => File::Spec::Unix->catfile(
+ $conf->_get_mirror('base'),
+ $data[2],
+ ), # extended path on the cpan mirror,
+ # like /A/AB/ABIGAIL
+ comment => $data[3], # comment on the module
+ author => $self->author_tree($author),
+ package => $package, # package name, like
+ # 'foo-bar-baz-1.03.tar.gz'
+ description => $dslip_tree->{ $data[0] }->{'description'},
+ dslip => $dslip,
+ _id => $self->_id, #id of this internals object
+ );
+
+ } #for
+
+ return $tree;
+
+} #_create_mod_tree
+
+=pod
+
+=head2 $cb->__create_dslip_tree([path => $path, uptodate => BOOL, verbose => BOOL])
+
+This method opens a source files and parses its contents into a
+searchable dslip-tree or restores a file-cached version of a
+previous parse, if the sources are uptodate and the file-cache exists.
+
+It takes the following arguments:
+
+=over 4
+
+=item uptodate
+
+A flag indicating whether the file-cache is uptodate or not.
+
+=item path
+
+The absolute path to the directory holding the source files.
+
+=item verbose
+
+A boolean flag indicating whether or not to be verbose.
+
+=back
+
+Will get information from the config file by default.
+
+Returns a tree on success, false on failure.
+
+=cut
+
+sub __create_dslip_tree {
+ my $self = shift;
+ my %hash = @_;
+ my $conf = $self->configure_object;
+
+ my $tmpl = {
+ path => { default => $conf->get_conf('base') },
+ verbose => { default => $conf->get_conf('verbose') },
+ uptodate => { default => 0 },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### get the file name of the source ###
+ my $file = File::Spec->catfile($args->{path}, $conf->_get_source('dslip'));
+
+ ### extract the file ###
+ my $ae = Archive::Extract->new( archive => $file ) or return;
+ my $out = STRIP_GZ_SUFFIX->($file);
+
+ ### make sure to set the PREFER_BIN flag if desired ###
+ { local $Archive::Extract::PREFER_BIN = $conf->get_conf('prefer_bin');
+ $ae->extract( to => $out ) or return;
+ }
+
+ my $in = $self->_get_file_contents( file => $out ) or return;
+
+ ### don't need it anymore ###
+ unlink $out;
+
+
+ ### get rid of the comments and the code ###
+ ### need a smarter parser, some people have this in their dslip info:
+ # [
+ # 'Statistics::LTU',
+ # 'R',
+ # 'd',
+ # 'p',
+ # 'O',
+ # '?',
+ # 'Implements Linear Threshold Units',
+ # ...skipping...
+ # "\x{c4}dd \x{fc}ml\x{e4}\x{fc}ts t\x{f6} \x{eb}v\x{eb}r\x{ff}th\x{ef}ng!",
+ # 'BENNIE',
+ # '11'
+ # ],
+ ### also, older versions say:
+ ### $cols = [....]
+ ### and newer versions say:
+ ### $CPANPLUS::Modulelist::cols = [...]
+ ### split '$cols' and '$data' into 2 variables ###
+ ### use this regex to make sure dslips with ';' in them don't cause
+ ### parser errors
+ my ($ds_one, $ds_two) = ($in =~ m|.+}\s+
+ (\$(?:CPAN::Modulelist::)?cols.*?)
+ (\$(?:CPAN::Modulelist::)?data.*)
+ |sx);
+
+ ### eval them into existence ###
+ ### still not too fond of this solution - kane ###
+ my ($cols, $data);
+ { #local $@; can't use this, it's buggy -kane
+
+ $cols = eval $ds_one;
+ error( loc("Error in eval of dslip source files: %1", $@) ) if $@;
+
+ $data = eval $ds_two;
+ error( loc("Error in eval of dslip source files: %1", $@) ) if $@;
+
+ }
+
+ my $tree = {};
+ my $primary = "modid";
+
+ ### this comes from CPAN::Modulelist
+ ### which is in 03modlist.data.gz
+ for (@$data){
+ my %hash;
+ @hash{@$cols} = @$_;
+ $tree->{$hash{$primary}} = \%hash;
+ }
+
+ return $tree;
+
+} #__create_dslip_tree
+
+=pod
+
+=head2 $cb->_dslip_defs ()
+
+This function returns the definition structure (ARRAYREF) of the
+dslip tree.
+
+=cut
+
+### these are the definitions used for dslip info
+### they shouldn't change over time.. so hardcoding them doesn't appear to
+### be a problem. if it is, we need to parse 03modlist.data better to filter
+### all this out.
+### right now, this is just used to look up dslip info from a module
+sub _dslip_defs {
+ my $self = shift;
+
+ my $aref = [
+
+ # D
+ [ q|Development Stage|, {
+ i => loc('Idea, listed to gain consensus or as a placeholder'),
+ c => loc('under construction but pre-alpha (not yet released)'),
+ a => loc('Alpha testing'),
+ b => loc('Beta testing'),
+ R => loc('Released'),
+ M => loc('Mature (no rigorous definition)'),
+ S => loc('Standard, supplied with Perl 5'),
+ }],
+
+ # S
+ [ q|Support Level|, {
+ m => loc('Mailing-list'),
+ d => loc('Developer'),
+ u => loc('Usenet newsgroup comp.lang.perl.modules'),
+ n => loc('None known, try comp.lang.perl.modules'),
+ a => loc('Abandoned; volunteers welcome to take over maintainance'),
+ }],
+
+ # L
+ [ q|Language Used|, {
+ p => loc('Perl-only, no compiler needed, should be platform independent'),
+ c => loc('C and perl, a C compiler will be needed'),
+ h => loc('Hybrid, written in perl with optional C code, no compiler needed'),
+ '+' => loc('C++ and perl, a C++ compiler will be needed'),
+ o => loc('perl and another language other than C or C++'),
+ }],
+
+ # I
+ [ q|Interface Style|, {
+ f => loc('plain Functions, no references used'),
+ h => loc('hybrid, object and function interfaces available'),
+ n => loc('no interface at all (huh?)'),
+ r => loc('some use of unblessed References or ties'),
+ O => loc('Object oriented using blessed references and/or inheritance'),
+ }],
+
+ # P
+ [ q|Public License|, {
+ p => loc('Standard-Perl: user may choose between GPL and Artistic'),
+ g => loc('GPL: GNU General Public License'),
+ l => loc('LGPL: "GNU Lesser General Public License" (previously known as "GNU Library General Public License")'),
+ b => loc('BSD: The BSD License'),
+ a => loc('Artistic license alone'),
+ o => loc('other (but distribution allowed without restrictions)'),
+ }],
+ ];
+
+ return $aref;
+}
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
+1;
diff --git a/lib/CPANPLUS/Internals/Utils.pm b/lib/CPANPLUS/Internals/Utils.pm
new file mode 100644
index 0000000000..625160831b
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Utils.pm
@@ -0,0 +1,536 @@
+package CPANPLUS::Internals::Utils;
+
+use strict;
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+use Cwd;
+use File::Copy;
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+local $Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Internals::Utils
+
+=head1 SYNOPSIS
+
+ my $bool = $cb->_mkdir( dir => 'blah' );
+ my $bool = $cb->_chdir( dir => 'blah' );
+ my $bool = $cb->_rmdir( dir => 'blah' );
+
+ my $bool = $cb->_move( from => '/some/file', to => '/other/file' );
+ my $bool = $cb->_move( from => '/some/dir', to => '/other/dir' );
+
+ my $cont = $cb->_get_file_contents( file => '/path/to/file' );
+
+
+ my $version = $cb->_perl_version( perl => $^X );
+
+=head1 DESCRIPTION
+
+C<CPANPLUS::Internals::Utils> holds a few convenience functions for
+CPANPLUS libraries.
+
+=head1 METHODS
+
+=head2 $cb->_mkdir( dir => '/some/dir' )
+
+C<_mkdir> creates a full path to a directory.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _mkdir {
+ my $self = shift;
+
+ my %hash = @_;
+
+ my $tmpl = {
+ dir => { required => 1 },
+ };
+
+ my $args = check( $tmpl, \%hash ) or (
+ error(loc( Params::Check->last_error ) ), return
+ );
+
+ unless( can_load( modules => { 'File::Path' => 0.0 } ) ) {
+ error( loc("Could not use File::Path! This module should be core!") );
+ return;
+ }
+
+ eval { File::Path::mkpath($args->{dir}) };
+
+ if($@) {
+ chomp($@);
+ error(loc(qq[Could not create directory '%1': %2], $args->{dir}, $@ ));
+ return;
+ }
+
+ return 1;
+}
+
+=pod
+
+=head2 $cb->_chdir( dir => '/some/dir' )
+
+C<_chdir> changes directory to a dir.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _chdir {
+ my $self = shift;
+ my %hash = @_;
+
+ my $tmpl = {
+ dir => { required => 1, allow => DIR_EXISTS },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ unless( chdir $args->{dir} ) {
+ error( loc(q[Could not chdir into '%1'], $args->{dir}) );
+ return;
+ }
+
+ return 1;
+}
+
+=pod
+
+=head2 $cb->_rmdir( dir => '/some/dir' );
+
+Removes a directory completely, even if it is non-empty.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _rmdir {
+ my $self = shift;
+ my %hash = @_;
+
+ my $tmpl = {
+ dir => { required => 1, allow => IS_DIR },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ unless( can_load( modules => { 'File::Path' => 0.0 } ) ) {
+ error( loc("Could not use File::Path! This module should be core!") );
+ return;
+ }
+
+ eval { File::Path::rmtree($args->{dir}) };
+
+ if($@) {
+ chomp($@);
+ error(loc(qq[Could not delete directory '%1': %2], $args->{dir}, $@ ));
+ return;
+ }
+
+ return 1;
+}
+
+=pod
+
+=head2 $cb->_perl_version ( perl => 'some/perl/binary' );
+
+C<_perl_version> returns the version of a certain perl binary.
+It does this by actually running a command.
+
+Returns the perl version on success and false on failure.
+
+=cut
+
+sub _perl_version {
+ my $self = shift;
+ my %hash = @_;
+
+ my $perl;
+ my $tmpl = {
+ perl => { required => 1, store => \$perl },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $perl_version;
+ ### special perl, or the one we are running under?
+ if( $perl eq $^X ) {
+ ### just load the config
+ require Config;
+ $perl_version = $Config::Config{version};
+
+ } else {
+ my $cmd = $perl .
+ ' -MConfig -eprint+Config::config_vars+version';
+ ($perl_version) = (`$cmd` =~ /version='(.*)'/);
+ }
+
+ return $perl_version if defined $perl_version;
+ return;
+}
+
+=pod
+
+=head2 $cb->_version_to_number( version => $version );
+
+Returns a proper module version, or '0.0' if none was available.
+
+=cut
+
+sub _version_to_number {
+ my $self = shift;
+ my %hash = @_;
+
+ my $version;
+ my $tmpl = {
+ version => { default => '0.0', store => \$version },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ return $version if $version =~ /^\.?\d/;
+ return '0.0';
+}
+
+=pod
+
+=head2 $cb->_whoami
+
+Returns the name of the subroutine you're currently in.
+
+=cut
+
+sub _whoami { my $name = (caller 1)[3]; $name =~ s/.+:://; $name }
+
+=pod
+
+=head2 _get_file_contents( file => $file );
+
+Returns the contents of a file
+
+=cut
+
+sub _get_file_contents {
+ my $self = shift;
+ my %hash = @_;
+
+ my $file;
+ my $tmpl = {
+ file => { required => 1, store => \$file }
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $fh = OPEN_FILE->($file) or return;
+ my $contents = do { local $/; <$fh> };
+
+ return $contents;
+}
+
+=pod $cb->_move( from => $file|$dir, to => $target );
+
+Moves a file or directory to the target.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _move {
+ my $self = shift;
+ my %hash = @_;
+
+ my $from; my $to;
+ my $tmpl = {
+ file => { required => 1, allow => [IS_FILE,IS_DIR],
+ store => \$from },
+ to => { required => 1, store => \$to }
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ if( File::Copy::move( $from, $to ) ) {
+ return 1;
+ } else {
+ error(loc("Failed to move '%1' to '%2': %3", $from, $to, $!));
+ return;
+ }
+}
+
+=pod $cb->_copy( from => $file|$dir, to => $target );
+
+Moves a file or directory to the target.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _copy {
+ my $self = shift;
+ my %hash = @_;
+
+ my($from,$to);
+ my $tmpl = {
+ file =>{ required => 1, allow => [IS_FILE,IS_DIR],
+ store => \$from },
+ to => { required => 1, store => \$to }
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ if( File::Copy::copy( $from, $to ) ) {
+ return 1;
+ } else {
+ error(loc("Failed to copy '%1' to '%2': %3", $from, $to, $!));
+ return;
+ }
+}
+
+=head2 $cb->_mode_plus_w( file => '/path/to/file' );
+
+Sets the +w bit for the file.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub _mode_plus_w {
+ my $self = shift;
+ my %hash = @_;
+
+ require File::stat;
+
+ my $file;
+ my $tmpl = {
+ file => { required => 1, allow => IS_FILE, store => \$file },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### set the mode to +w for a file and +wx for a dir
+ my $x = File::stat::stat( $file );
+ my $mask = -d $file ? 0100 : 0200;
+
+ if( $x and chmod( $x->mode|$mask, $file ) ) {
+ return 1;
+
+ } else {
+ error(loc("Failed to '%1' '%2': '%3'", 'chmod +w', $file, $!));
+ return;
+ }
+}
+
+=head2 $uri = $cb->_host_to_uri( scheme => SCHEME, host => HOST, path => PATH );
+
+Turns a CPANPLUS::Config style C<host> entry into an URI string.
+
+Returns the uri on success, and false on failure
+
+=cut
+
+sub _host_to_uri {
+ my $self = shift;
+ my %hash = @_;
+
+ my($scheme, $host, $path);
+ my $tmpl = {
+ scheme => { required => 1, store => \$scheme },
+ host => { default => '', store => \$host },
+ path => { default => '', store => \$path },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ $host ||= 'localhost';
+
+ return "$scheme://" . File::Spec::Unix->catdir( $host, $path );
+}
+
+=head2 $cb->_vcmp( VERSION, VERSION );
+
+Normalizes the versions passed and does a '<=>' on them, returning the result.
+
+=cut
+
+sub _vcmp {
+ my $self = shift;
+ my ($x, $y) = @_;
+
+ s/_//g foreach $x, $y;
+
+ return $x <=> $y;
+}
+
+=head2 $cb->_home_dir
+
+Returns the user's homedir, or C<cwd> if it could not be found
+
+=cut
+
+sub _home_dir {
+ my @os_home_envs = qw( APPDATA HOME USERPROFILE WINDIR SYS$LOGIN );
+
+ for my $env ( @os_home_envs ) {
+ next unless exists $ENV{ $env };
+ next unless defined $ENV{ $env } && length $ENV{ $env };
+ return $ENV{ $env } if -d $ENV{ $env };
+ }
+
+ return cwd();
+}
+
+=head2 $path = $cb->_safe_path( path => $path );
+
+Returns a path that's safe to us on Win32. Only cleans up
+the path on Win32 if the path exists.
+
+=cut
+
+sub _safe_path {
+ my $self = shift;
+
+ my %hash = @_;
+
+ my $path;
+ my $tmpl = {
+ path => { required => 1, store => \$path },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### only need to fix it up if there's spaces in the path
+ return $path unless $path =~ /\s+/;
+
+ ### or if we are on win32
+ return $path if $^O ne 'MSWin32';
+
+ ### clean up paths if we are on win32
+ return Win32::GetShortPathName( $path ) || $path;
+
+}
+
+
+=head2 ($pkg, $version, $ext) = $cb->_split_package_string( package => PACKAGE_STRING );
+
+Splits the name of a CPAN package string up in it's package, version
+and extension parts.
+
+For example, C<Foo-Bar-1.2.tar.gz> would return the following parts:
+
+ Package: Foo-Bar
+ Version: 1.2
+ Extension: tar.gz
+
+=cut
+
+{ my $del_re = qr/[-_\+]/i; # delimiter between elements
+ my $pkg_re = qr/[a-z] # any letters followed by
+ [a-z\d]* # any letters, numbers
+ (?i:\.pm)? # followed by '.pm'--authors do this :(
+ (?: # optionally repeating:
+ $del_re # followed by a delimiter
+ [a-z] # any letters followed by
+ [a-z\d]* # any letters, numbers
+ (?i:\.pm)? # followed by '.pm'--authors do this :(
+ )*
+ /xi;
+
+ my $ver_re = qr/[a-z]*\d+[a-z]* # contains a digit and possibly letters
+ (?:
+ [-._] # followed by a delimiter
+ [a-z\d]+ # and more digits and or letters
+ )*?
+ /xi;
+
+ my $ext_re = qr/[a-z] # a letter, followed by
+ [a-z\d]* # letters and or digits, optionally
+ (?:
+ \. # followed by a dot and letters
+ [a-z\d]+ # and or digits (like .tar.bz2)
+ )? # optionally
+ /xi;
+
+ my $ver_ext_re = qr/
+ ($ver_re+) # version, optional
+ (?:
+ \. # a literal .
+ ($ext_re) # extension,
+ )? # optional, but requires version
+ /xi;
+
+ ### composed regex for CPAN packages
+ my $full_re = qr/
+ ^
+ ($pkg_re+) # package
+ (?:
+ $del_re # delimiter
+ $ver_ext_re # version + extension
+ )?
+ $
+ /xi;
+
+ ### composed regex for perl packages
+ my $perl = PERL_CORE;
+ my $perl_re = qr/
+ ^
+ ($perl) # package name for 'perl'
+ (?:
+ $ver_ext_re # version + extension
+ )?
+ $
+ /xi;
+
+
+sub _split_package_string {
+ my $self = shift;
+ my %hash = @_;
+
+ my $str;
+ my $tmpl = { package => { required => 1, store => \$str } };
+ check( $tmpl, \%hash ) or return;
+
+
+ ### 2 different regexes, one for the 'perl' package,
+ ### one for ordinary CPAN packages.. try them both,
+ ### first match wins.
+ for my $re ( $full_re, $perl_re ) {
+
+ ### try the next if the match fails
+ $str =~ $re or next;
+
+ my $pkg = $1 || '';
+ my $ver = $2 || '';
+ my $ext = $3 || '';
+
+ ### this regex resets the capture markers!
+ ### strip the trailing delimiter
+ $pkg =~ s/$del_re$//;
+
+ ### strip the .pm package suffix some authors insist on adding
+ $pkg =~ s/\.pm$//i;
+
+ return ($pkg, $ver, $ext );
+ }
+
+ return;
+ }
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Internals/Utils/Autoflush.pm b/lib/CPANPLUS/Internals/Utils/Autoflush.pm
new file mode 100644
index 0000000000..56566436a1
--- /dev/null
+++ b/lib/CPANPLUS/Internals/Utils/Autoflush.pm
@@ -0,0 +1,5 @@
+package CPANPLUS::Internals::Utils::Autoflush;
+
+BEGIN { $|++ };
+
+1;
diff --git a/lib/CPANPLUS/Module.pm b/lib/CPANPLUS/Module.pm
new file mode 100644
index 0000000000..96030d30e6
--- /dev/null
+++ b/lib/CPANPLUS/Module.pm
@@ -0,0 +1,1580 @@
+package CPANPLUS::Module;
+
+use strict;
+use vars qw[@ISA];
+
+
+use CPANPLUS::Dist;
+use CPANPLUS::Error;
+use CPANPLUS::Module::Signature;
+use CPANPLUS::Module::Checksums;
+use CPANPLUS::Internals::Constants;
+
+use FileHandle;
+
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+use IPC::Cmd qw[can_run run];
+use File::Find qw[find];
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load check_install];
+
+$Params::Check::VERBOSE = 1;
+
+@ISA = qw[ CPANPLUS::Module::Signature CPANPLUS::Module::Checksums];
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Module
+
+=head1 SYNOPSIS
+
+ ### get a module object from the CPANPLUS::Backend object
+ my $mod = $cb->module_tree('Some::Module');
+
+ ### accessors
+ $mod->version;
+ $mod->package;
+
+ ### methods
+ $mod->fetch;
+ $mod->extract;
+ $mod->install;
+
+
+=head1 DESCRIPTION
+
+C<CPANPLUS::Module> creates objects from the information in the
+source files. These can then be used to query and perform actions
+on, like fetching or installing.
+
+These objects should only be created internally. For C<fake> objects,
+there's the C<CPANPLUS::Module::Fake> class. To obtain a module object
+consult the C<CPANPLUS::Backend> documentation.
+
+=cut
+
+my $tmpl = {
+ module => { default => '', required => 1 }, # full module name
+ version => { default => '0.0' }, # version number
+ path => { default => '', required => 1 }, # extended path on the
+ # cpan mirror, like
+ # /author/id/K/KA/KANE
+ comment => { default => ''}, # comment on module
+ package => { default => '', required => 1 }, # package name, like
+ # 'bar-baz-1.03.tgz'
+ description => { default => '' }, # description of the
+ # module
+ dslip => { default => ' ' }, # dslip information
+ _id => { required => 1 }, # id of the Internals
+ # parent object
+ _status => { no_override => 1 }, # stores status object
+ author => { default => '', required => 1,
+ allow => IS_AUTHOBJ }, # module author
+ mtime => { default => '' },
+};
+
+### autogenerate accessors ###
+for my $key ( keys %$tmpl ) {
+ no strict 'refs';
+ *{__PACKAGE__."::$key"} = sub {
+ $_[0]->{$key} = $_[1] if @_ > 1;
+ return $_[0]->{$key};
+ }
+}
+
+=pod
+
+=head1 CLASS METHODS
+
+=head2 accessors ()
+
+Returns a list of all accessor methods to the object
+
+=cut
+
+### *name is an alias, include it explicitly
+sub accessors { return ('name', keys %$tmpl) };
+
+=head1 ACCESSORS
+
+An objects of this class has the following accessors:
+
+=over 4
+
+=item name
+
+Name of the module.
+
+=item module
+
+Name of the module.
+
+=item version
+
+Version of the module. Defaults to '0.0' if none was provided.
+
+=item path
+
+Extended path on the mirror.
+
+=item comment
+
+Any comment about the module -- largely unused.
+
+=item package
+
+The name of the package.
+
+=item description
+
+Description of the module -- only registered modules have this.
+
+=item dslip
+
+The five character dslip string, that represents meta-data of the
+module -- again, only registered modules have this.
+
+=item status
+
+The C<CPANPLUS::Module::Status> object associated with this object.
+(see below).
+
+=item author
+
+The C<CPANPLUS::Module::Author> object associated with this object.
+
+=item parent
+
+The C<CPANPLUS::Internals> object that spawned this module object.
+
+=back
+
+=cut
+
+### Alias ->name to ->module, for human beings.
+*name = *module;
+
+sub parent {
+ my $self = shift;
+ my $obj = CPANPLUS::Internals->_retrieve_id( $self->_id );
+
+ return $obj;
+}
+
+=head1 STATUS ACCESSORS
+
+C<CPANPLUS> caches a lot of results from method calls and saves data
+it collected along the road for later reuse.
+
+C<CPANPLUS> uses this internally, but it is also available for the end
+user. You can get a status object by calling:
+
+ $modobj->status
+
+You can then query the object as follows:
+
+=over 4
+
+=item installer_type
+
+The installer type used for this distribution. Will be one of
+'makemaker' or 'build'. This determines whether C<CPANPLUS::Dist::MM>
+or C<CPANPLUS::Dist::Build> will be used to build this distribution.
+
+=item dist_cpan
+
+The dist object used to do the CPAN-side of the installation. Either
+a C<CPANPLUS::Dist::MM> or C<CPANPLUS::Dist::Build> object.
+
+=item dist
+
+The custom dist object used to do the operating specific side of the
+installation, if you've chosen to use this. For example, if you've
+chosen to install using the C<ports> format, this may be a
+C<CPANPLUS::Dist::Ports> object.
+
+Undefined if you didn't specify a separate format to install through.
+
+=item prereqs
+
+A hashref of prereqs this distribution was found to have. Will look
+something like this:
+
+ { Carp => 0.01, strict => 0 }
+
+Might be undefined if the distribution didn't have any prerequisites.
+
+=item signature
+
+Flag indicating, if a signature check was done, whether it was OK or
+not.
+
+=item extract
+
+The directory this distribution was extracted to.
+
+=item fetch
+
+The location this distribution was fetched to.
+
+=item readme
+
+The text of this distributions README file.
+
+=item uninstall
+
+Flag indicating if an uninstall call was done successfully.
+
+=item created
+
+Flag indicating if the C<create> call to your dist object was done
+successfully.
+
+=item installed
+
+Flag indicating if the C<install> call to your dist object was done
+successfully.
+
+=item checksums
+
+The location of this distributions CHECKSUMS file.
+
+=item checksum_ok
+
+Flag indicating if the checksums check was done successfully.
+
+=item checksum_value
+
+The checksum value this distribution is expected to have
+
+=back
+
+=head1 METHODS
+
+=head2 $self = CPANPLUS::Module::new( OPTIONS )
+
+This method returns a C<CPANPLUS::Module> object. Normal users
+should never call this method directly, but instead use the
+C<CPANPLUS::Backend> to obtain module objects.
+
+This example illustrates a C<new()> call with all required arguments:
+
+ CPANPLUS::Module->new(
+ module => 'Foo',
+ path => 'authors/id/A/AA/AAA',
+ package => 'Foo-1.0.tgz',
+ author => $author_object,
+ _id => INTERNALS_OBJECT_ID,
+ );
+
+Every accessor is also a valid option to pass to C<new>.
+
+Returns a module object on success and false on failure.
+
+=cut
+
+
+sub new {
+ my($class, %hash) = @_;
+
+ ### don't check the template for sanity
+ ### -- we know it's good and saves a lot of performance
+ local $Params::Check::SANITY_CHECK_TEMPLATE = 0;
+
+ my $object = check( $tmpl, \%hash ) or return;
+
+ bless $object, $class;
+
+ return $object;
+}
+
+### only create status objects when they're actually asked for
+sub status {
+ my $self = shift;
+ return $self->_status if $self->_status;
+
+ my $acc = Object::Accessor->new;
+ $acc->mk_accessors( qw[ installer_type dist_cpan dist prereqs
+ signature extract fetch readme uninstall
+ created installed prepared checksums files
+ checksum_ok checksum_value _fetch_from] );
+
+ $self->_status( $acc );
+
+ return $self->_status;
+}
+
+
+### flush the cache of this object ###
+sub _flush {
+ my $self = shift;
+ $self->status->mk_flush;
+ return 1;
+}
+
+=head2 $mod->package_name
+
+Returns the name of the package a module is in. For C<Acme::Bleach>
+that might be C<Acme-Bleach>.
+
+=head2 $mod->package_version
+
+Returns the version of the package a module is in. For a module
+in the package C<Acme-Bleach-1.1.tar.gz> this would be C<1.1>.
+
+=head2 $mod->package_extension
+
+Returns the suffix added by the compression method of a package a
+certain module is in. For a module in C<Acme-Bleach-1.1.tar.gz>, this
+would be C<tar.gz>.
+
+=head2 $mod->package_is_perl_core
+
+Returns a boolean indicating of the package a particular module is in,
+is actually a core perl distribution.
+
+=head2 $mod->module_is_supplied_with_perl_core( [version => $]] )
+
+Returns a boolean indicating whether C<ANY VERSION> of this module
+was supplied with the current running perl's core package.
+
+=head2 $mod->is_bundle
+
+Returns a boolean indicating if the module you are looking at, is
+actually a bundle. Bundles are identified as modules whose name starts
+with C<Bundle::>.
+
+=head2 $mod->is_third_party
+
+Returns a boolean indicating whether the package is a known third-party
+module (i.e. it's not provided by the standard Perl distribution and
+is not available on the CPAN, but on a third party software provider).
+See L<Module::ThirdParty> for more details.
+
+=head2 $mod->third_party_information
+
+Returns a reference to a hash with more information about a third-party
+module. See the documentation about C<module_information()> in
+L<Module::ThirdParty> for more details.
+
+=cut
+
+{ ### fetches the test reports for a certain module ###
+ my %map = (
+ name => 0,
+ version => 1,
+ extension => 2,
+ );
+
+ while ( my($type, $index) = each %map ) {
+ my $name = 'package_' . $type;
+
+ no strict 'refs';
+ *$name = sub {
+ my $self = shift;
+ my @res = $self->parent->_split_package_string(
+ package => $self->package
+ );
+
+ ### return the corresponding index from the result
+ return $res[$index] if @res;
+ return;
+ };
+ }
+
+ sub package_is_perl_core {
+ my $self = shift;
+
+ ### check if the package looks like a perl core package
+ return 1 if $self->package_name eq PERL_CORE;
+
+ my $core = $self->module_is_supplied_with_perl_core;
+ ### ok, so it's found in the core, BUT it could be dual-lifed
+ if ($core) {
+ ### if the package is newer than installed, then it's dual-lifed
+ return if $self->version > $self->installed_version;
+
+ ### if the package is newer or equal to the corelist,
+ ### then it's dual-lifed
+ return if $self->version >= $core;
+
+ ### otherwise, it's older than corelist, thus unsuitable.
+ return 1;
+ }
+
+ ### not in corelist, not a perl core package.
+ return;
+ }
+
+ sub module_is_supplied_with_perl_core {
+ my $self = shift;
+ my $ver = shift || $];
+
+ ### check Module::CoreList to see if it's a core package
+ require Module::CoreList;
+ my $core = $Module::CoreList::version{ $ver }->{ $self->module };
+
+ return $core;
+ }
+
+ ### make sure Bundle-Foo also gets flagged as bundle
+ sub is_bundle {
+ return shift->module =~ /^bundle(?:-|::)/i ? 1 : 0;
+ }
+
+ sub is_third_party {
+ my $self = shift;
+
+ return unless can_load( modules => { 'Module::ThirdParty' => 0 } );
+
+ return Module::ThirdParty::is_3rd_party( $self->name );
+ }
+
+ sub third_party_information {
+ my $self = shift;
+
+ return unless $self->is_third_party;
+
+ return Module::ThirdParty::module_information( $self->name );
+ }
+}
+
+=pod
+
+=head2 $clone = $self->clone
+
+Clones the current module object for tinkering with.
+It will have a clean C<CPANPLUS::Module::Status> object, as well as
+a fake C<CPANPLUS::Module::Author> object.
+
+=cut
+
+sub clone {
+ my $self = shift;
+
+ ### clone the object ###
+ my %data;
+ for my $acc ( grep !/status/, __PACKAGE__->accessors() ) {
+ $data{$acc} = $self->$acc();
+ }
+
+ my $obj = CPANPLUS::Module::Fake->new( %data );
+
+ return $obj;
+}
+
+=pod
+
+=head2 $where = $self->fetch
+
+Fetches the module from a CPAN mirror.
+Look at L<CPANPLUS::Internals::Fetch::_fetch()> for details on the
+options you can pass.
+
+=cut
+
+sub fetch {
+ my $self = shift;
+ my $cb = $self->parent;
+
+ ### custom args
+ my %args = ( module => $self );
+
+ ### if a custom fetch location got specified before, add that here
+ $args{fetch_from} = $self->status->_fetch_from
+ if $self->status->_fetch_from;
+
+ my $where = $cb->_fetch( @_, %args ) or return;
+
+ ### do an md5 check ###
+ if( !$self->status->_fetch_from and
+ $cb->configure_object->get_conf('md5') and
+ $self->package ne CHECKSUMS
+ ) {
+ unless( $self->_validate_checksum ) {
+ error( loc( "Checksum error for '%1' -- will not trust package",
+ $self->package) );
+ return;
+ }
+ }
+
+ return $where;
+}
+
+=pod
+
+=head2 $path = $self->extract
+
+Extracts the fetched module.
+Look at L<CPANPLUS::Internals::Extract::_extract()> for details on
+the options you can pass.
+
+=cut
+
+sub extract {
+ my $self = shift;
+ my $cb = $self->parent;
+
+ unless( $self->status->fetch ) {
+ error( loc( "You have not fetched '%1' yet -- cannot extract",
+ $self->module) );
+ return;
+ }
+
+ return $cb->_extract( @_, module => $self );
+}
+
+=head2 $type = $self->get_installer_type([prefer_makefile => BOOL])
+
+Gets the installer type for this module. This may either be C<build> or
+C<makemaker>. If C<Module::Build> is unavailable or no installer type
+is available, it will fall back to C<makemaker>. If both are available,
+it will pick the one indicated by your config, or by the
+C<prefer_makefile> option you can pass to this function.
+
+Returns the installer type on success, and false on error.
+
+=cut
+
+sub get_installer_type {
+ my $self = shift;
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $prefer_makefile;
+ my $tmpl = {
+ prefer_makefile => { default => $conf->get_conf('prefer_makefile'),
+ store => \$prefer_makefile, allow => BOOLEANS },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $extract = $self->status->extract();
+ unless( $extract ) {
+ error(loc("Cannot determine installer type of unextracted module '%1'",
+ $self->module));
+ return;
+ }
+
+
+ ### check if it's a makemaker or a module::build type dist ###
+ my $found_build = -e BUILD_PL->( $extract );
+ my $found_makefile = -e MAKEFILE_PL->( $extract );
+
+ my $type;
+ $type = INSTALLER_BUILD if !$prefer_makefile && $found_build;
+ $type = INSTALLER_BUILD if $found_build && !$found_makefile;
+ $type = INSTALLER_MM if $prefer_makefile && $found_makefile;
+ $type = INSTALLER_MM if $found_makefile && !$found_build;
+
+ ### ok, so it's a 'build' installer, but you don't /have/ module build
+ if( $type eq INSTALLER_BUILD and (
+ not grep { $_ eq INSTALLER_BUILD } CPANPLUS::Dist->dist_types )
+ ) {
+ error( loc( "This module requires '%1' and '%2' to be installed, ".
+ "but you don't have it! Will fall back to ".
+ "'%3', but might not be able to install!",
+ 'Module::Build', INSTALLER_BUILD, INSTALLER_MM ) );
+ $type = INSTALLER_MM;
+
+ ### ok, actually we found neither ###
+ } elsif ( !$type ) {
+ error( loc( "Unable to find '%1' or '%2' for '%3'; ".
+ "Will default to '%4' but might be unable ".
+ "to install!", BUILD_PL->(), MAKEFILE_PL->(),
+ $self->module, INSTALLER_MM ) );
+ $type = INSTALLER_MM;
+ }
+
+ return $self->status->installer_type( $type ) if $type;
+ return;
+}
+
+=pod
+
+=head2 $dist = $self->dist([target => 'prepare|create', format => DISTRIBUTION_TYPE, args => {key => val}]);
+
+Create a distribution object, ready to be installed.
+Distribution type defaults to your config settings
+
+The optional C<args> hashref is passed on to the specific distribution
+types' C<create> method after being dereferenced.
+
+Returns a distribution object on success, false on failure.
+
+See C<CPANPLUS::Dist> for details.
+
+=cut
+
+sub dist {
+ my $self = shift;
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ ### have you determined your installer type yet? if not, do it here,
+ ### we need the info
+ $self->get_installer_type unless $self->status->installer_type;
+
+
+ my($type,$args,$target);
+ my $tmpl = {
+ format => { default => $conf->get_conf('dist_type') ||
+ $self->status->installer_type,
+ store => \$type },
+ target => { default => TARGET_CREATE, store => \$target },
+ args => { default => {}, store => \$args },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $dist = CPANPLUS::Dist->new(
+ format => $type,
+ module => $self
+ ) or return;
+
+ my $dist_cpan = $type eq $self->status->installer_type
+ ? $dist
+ : CPANPLUS::Dist->new(
+ format => $self->status->installer_type,
+ module => $self,
+ );
+
+ ### store the dists
+ $self->status->dist_cpan( $dist_cpan );
+ $self->status->dist( $dist );
+
+ DIST: {
+ ### first prepare the dist
+ $dist->prepare( %$args ) or return;
+ $self->status->prepared(1);
+
+ ### you just wanted us to prepare?
+ last DIST if $target eq TARGET_PREPARE;
+
+ $dist->create( %$args ) or return;
+ $self->status->created(1);
+ }
+
+ return $dist;
+}
+
+=pod
+
+=head2 $bool = $mod->prepare( )
+
+Convenience method around C<install()> that prepares a module
+without actually building it. This is equivalent to invoking C<install>
+with C<target> set to C<prepare>
+
+Returns true on success, false on failure.
+
+=cut
+
+sub prepare {
+ my $self = shift;
+ return $self->install( @_, target => TARGET_PREPARE );
+}
+
+=head2 $bool = $mod->create( )
+
+Convenience method around C<install()> that creates a module.
+This is equivalent to invoking C<install> with C<target> set to
+C<create>
+
+Returns true on success, false on failure.
+
+=cut
+
+sub create {
+ my $self = shift;
+ return $self->install( @_, target => TARGET_CREATE );
+}
+
+=head2 $bool = $mod->test( )
+
+Convenience wrapper around C<install()> that tests a module, without
+installing it.
+It's the equivalent to invoking C<install()> with C<target> set to
+C<create> and C<skiptest> set to C<0>.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub test {
+ my $self = shift;
+ return $self->install( @_, target => TARGET_CREATE, skiptest => 0 );
+}
+
+=pod
+
+=head2 $bool = $self->install([ target => 'prepare|create|install', format => FORMAT_TYPE, extractdir => DIRECTORY, fetchdir => DIRECTORY, prefer_bin => BOOL, force => BOOL, verbose => BOOL, ..... ]);
+
+Installs the current module. This includes fetching it and extracting
+it, if this hasn't been done yet, as well as creating a distribution
+object for it.
+
+This means you can pass it more arguments than described above, which
+will be passed on to the relevant methods as they are called.
+
+See C<CPANPLUS::Internals::Fetch>, C<CPANPLUS::Internals::Extract> and
+C<CPANPLUS::Dist> for details.
+
+Returns true on success, false on failure.
+
+=cut
+
+sub install {
+ my $self = shift;
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $args; my $target; my $format;
+ { ### so we can use the rest of the args to the create calls etc ###
+ local $Params::Check::NO_DUPLICATES = 1;
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ ### targets 'dist' and 'test' are now completely ignored ###
+ my $tmpl = {
+ ### match this allow list with Dist->_resolve_prereqs
+ target => { default => TARGET_INSTALL, store => \$target,
+ allow => [TARGET_PREPARE, TARGET_CREATE,
+ TARGET_INSTALL] },
+ force => { default => $conf->get_conf('force'), },
+ verbose => { default => $conf->get_conf('verbose'), },
+ format => { default => $conf->get_conf('dist_type'),
+ store => \$format },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+
+ ### if this target isn't 'install', we will need to at least 'create'
+ ### every prereq, so it can build
+ ### XXX prereq_target of 'prepare' will do weird things here, and is
+ ### not supported.
+ $args->{'prereq_target'} ||= TARGET_CREATE if $target ne TARGET_INSTALL;
+
+ ### check if it's already upto date ###
+ if( $target eq TARGET_INSTALL and !$args->{'force'} and
+ !$self->package_is_perl_core() and # separate rules apply
+ ( $self->status->installed() or $self->is_uptodate ) and
+ !INSTALL_VIA_PACKAGE_MANAGER->($format)
+ ) {
+ msg(loc("Module '%1' already up to date, won't install without force",
+ $self->module), $args->{'verbose'} );
+ return $self->status->installed(1);
+ }
+
+ # if it's a non-installable core package, abort the install.
+ if( $self->package_is_perl_core() ) {
+ # if the installed is newer, say so.
+ if( $self->installed_version > $self->version ) {
+ error(loc("The core Perl %1 module '%2' (%3) is more ".
+ "recent than the latest release on CPAN (%4). ".
+ "Aborting install.",
+ $], $self->module, $self->installed_version,
+ $self->version ) );
+ # if the installed matches, say so.
+ } elsif( $self->installed_version == $self->version ) {
+ error(loc("The core Perl %1 module '%2' (%3) can only ".
+ "be installed by Perl itself. ".
+ "Aborting install.",
+ $], $self->module, $self->installed_version ) );
+ # otherwise, the installed is older; say so.
+ } else {
+ error(loc("The core Perl %1 module '%2' can only be ".
+ "upgraded from %3 to %4 by Perl itself (%5). ".
+ "Aborting install.",
+ $], $self->module, $self->installed_version,
+ $self->version, $self->package ) );
+ }
+ return;
+
+ ### it might be a known 3rd party module
+ } elsif ( $self->is_third_party ) {
+ my $info = $self->third_party_information;
+ error(loc(
+ "%1 is a known third-party module.\n\n".
+ "As it isn't available on the CPAN, CPANPLUS can't install " .
+ "it automatically. Therefore you need to install it manually " .
+ "before proceeding.\n\n".
+ "%2 is part of %3, published by %4, and should be available ".
+ "for download at the following address:\n\t%5",
+ $self->name, $self->name, $info->{name}, $info->{author},
+ $info->{url}
+ ));
+
+ return;
+ }
+
+ ### fetch it if need be ###
+ unless( $self->status->fetch ) {
+ my $params;
+ for (qw[prefer_bin fetchdir]) {
+ $params->{$_} = $args->{$_} if exists $args->{$_};
+ }
+ for (qw[force verbose]) {
+ $params->{$_} = $args->{$_} if defined $args->{$_};
+ }
+ $self->fetch( %$params ) or return;
+ }
+
+ ### extract it if need be ###
+ unless( $self->status->extract ) {
+ my $params;
+ for (qw[prefer_bin extractdir]) {
+ $params->{$_} = $args->{$_} if exists $args->{$_};
+ }
+ for (qw[force verbose]) {
+ $params->{$_} = $args->{$_} if defined $args->{$_};
+ }
+ $self->extract( %$params ) or return;
+ }
+
+ $format ||= $self->status->installer_type;
+
+ unless( $format ) {
+ error( loc( "Don't know what installer to use; " .
+ "Couldn't find either '%1' or '%2' in the extraction " .
+ "directory '%3' -- will be unable to install",
+ BUILD_PL->(), MAKEFILE_PL->(), $self->status->extract ) );
+
+ $self->status->installed(0);
+ return;
+ }
+
+
+ ### do SIGNATURE checks? ###
+ if( $conf->get_conf('signature') ) {
+ unless( $self->check_signature( verbose => $args->{verbose} ) ) {
+ error( loc( "Signature check failed for module '%1' ".
+ "-- Not trusting this module, aborting install",
+ $self->module ) );
+ $self->status->signature(0);
+
+ ### send out test report on broken sig
+ if( $conf->get_conf('cpantest') ) {
+ $cb->_send_report(
+ module => $self,
+ failed => 1,
+ buffer => CPANPLUS::Error->stack_as_string,
+ verbose => $args->{verbose},
+ force => $args->{force},
+ ) or error(loc("Failed to send test report for '%1'",
+ $self->module ) );
+ }
+
+ return;
+
+ } else {
+ ### signature OK ###
+ $self->status->signature(1);
+ }
+ }
+
+ ### a target of 'create' basically means not to run make test ###
+ ### eh, no it /doesn't/.. skiptest => 1 means skiptest => 1.
+ #$args->{'skiptest'} = 1 if $target eq 'create';
+
+ ### bundle rules apply ###
+ if( $self->is_bundle ) {
+ ### check what we need to install ###
+ my @prereqs = $self->bundle_modules();
+ unless( @prereqs ) {
+ error( loc( "Bundle '%1' does not specify any modules to install",
+ $self->module ) );
+
+ ### XXX mark an error here? ###
+ }
+ }
+
+ my $dist = $self->dist( format => $format,
+ target => $target,
+ args => $args );
+ unless( $dist ) {
+ error( loc( "Unable to create a new distribution object for '%1' " .
+ "-- cannot continue", $self->module ) );
+ return;
+ }
+
+ return 1 if $target ne TARGET_INSTALL;
+
+ my $ok = $dist->install( %$args ) ? 1 : 0;
+
+ $self->status->installed($ok);
+
+ return 1 if $ok;
+ return;
+}
+
+=pod @list = $self->bundle_modules()
+
+Returns a list of module objects the Bundle specifies.
+
+This requires you to have extracted the bundle already, using the
+C<extract()> method.
+
+Returns false on error.
+
+=cut
+
+sub bundle_modules {
+ my $self = shift;
+ my $cb = $self->parent;
+
+ unless( $self->is_bundle ) {
+ error( loc("'%1' is not a bundle", $self->module ) );
+ return;
+ }
+
+ my $dir;
+ unless( $dir = $self->status->extract ) {
+ error( loc("Don't know where '%1' was extracted to", $self->module ) );
+ return;
+ }
+
+ my @files;
+ find( {
+ wanted => sub { push @files, File::Spec->rel2abs($_) if /\.pm/i; },
+ no_chdir => 1,
+ }, $dir );
+
+ my $prereqs = {}; my @list; my $seen = {};
+ for my $file ( @files ) {
+ my $fh = FileHandle->new($file)
+ or( error(loc("Could not open '%1' for reading: %2",
+ $file,$!)), next );
+
+ my $flag;
+ while(<$fh>) {
+ ### quick hack to read past the header of the file ###
+ last if $flag && m|^=head|i;
+
+ ### from perldoc cpan:
+ ### =head1 CONTENTS
+ ### In this pod section each line obeys the format
+ ### Module_Name [Version_String] [- optional text]
+ $flag = 1 if m|^=head1 CONTENTS|i;
+
+ if ($flag && /^(?!=)(\S+)\s*(\S+)?/) {
+ my $module = $1;
+ my $version = $2 || '0';
+
+ my $obj = $cb->module_tree($module);
+
+ unless( $obj ) {
+ error(loc("Cannot find bundled module '%1'", $module),
+ loc("-- it does not seem to exist") );
+ next;
+ }
+
+ ### make sure we list no duplicates ###
+ unless( $seen->{ $obj->module }++ ) {
+ push @list, $obj;
+ $prereqs->{ $module } =
+ $cb->_version_to_number( version => $version );
+ }
+ }
+ }
+ }
+
+ ### store the prereqs we just found ###
+ $self->status->prereqs( $prereqs );
+
+ return @list;
+}
+
+=pod
+
+=head2 $text = $self->readme
+
+Fetches the readme belonging to this module and stores it under
+C<< $obj->status->readme >>. Returns the readme as a string on
+success and returns false on failure.
+
+=cut
+
+sub readme {
+ my $self = shift;
+ my $conf = $self->parent->configure_object;
+
+ ### did we already dl the readme once? ###
+ return $self->status->readme() if $self->status->readme();
+
+ ### this should be core ###
+ return unless can_load( modules => { FileHandle => '0.0' },
+ verbose => 1,
+ );
+
+ ### get a clone of the current object, with a fresh status ###
+ my $obj = $self->clone or return;
+
+ ### munge the package name
+ my $pkg = README->( $obj );
+ $obj->package($pkg);
+
+ my $file;
+ { ### disable checksum fetches on readme downloads
+
+ my $tmp = $conf->get_conf( 'md5' );
+ $conf->set_conf( md5 => 0 );
+
+ $file = $obj->fetch;
+
+ $conf->set_conf( md5 => $tmp );
+
+ return unless $file;
+ }
+
+ ### read the file into a scalar, to store in the original object ###
+ my $fh = new FileHandle;
+ unless( $fh->open($file) ) {
+ error( loc( "Could not open file '%1': %2", $file, $! ) );
+ return;
+ }
+
+ my $in;
+ { local $/; $in = <$fh> };
+ $fh->close;
+
+ return $self->status->readme( $in );
+}
+
+=pod
+
+=head2 $version = $self->installed_version()
+
+Returns the currently installed version of this module, if any.
+
+=head2 $where = $self->installed_file()
+
+Returns the location of the currently installed file of this module,
+if any.
+
+=head2 $bool = $self->is_uptodate([version => VERSION_NUMBER])
+
+Returns a boolean indicating if this module is uptodate or not.
+
+=cut
+
+### uptodate/installed functions
+{ my $map = { # hashkey, alternate rv
+ installed_version => ['version', 0 ],
+ installed_file => ['file', ''],
+ is_uptodate => ['uptodate', 0 ],
+ };
+
+ while( my($method, $aref) = each %$map ) {
+ my($key,$alt_rv) = @$aref;
+
+ no strict 'refs';
+ *$method = sub {
+ ### never use the @INC hooks to find installed versions of
+ ### modules -- they're just there in case they're not on the
+ ### perl install, but the user shouldn't trust them for *other*
+ ### modules!
+ ### XXX CPANPLUS::inc is now obsolete, so this should not
+ ### be needed anymore
+ #local @INC = CPANPLUS::inc->original_inc;
+
+ my $self = shift;
+
+ ### make sure check_install is not looking in %INC, as
+ ### that may contain some of our sneakily loaded modules
+ ### that aren't installed as such. -- kane
+ local $Module::Load::Conditional::CHECK_INC_HASH = 0;
+ my $href = check_install(
+ module => $self->module,
+ version => $self->version,
+ @_,
+ );
+
+ return $href->{$key} || $alt_rv;
+ }
+ }
+}
+
+
+
+=pod
+
+=head2 $href = $self->details()
+
+Returns a hashref with key/value pairs offering more information about
+a particular module. For example, for C<Time::HiRes> it might look like
+this:
+
+ Author Jarkko Hietaniemi (jhi@iki.fi)
+ Description High resolution time, sleep, and alarm
+ Development Stage Released
+ Installed File /usr/local/perl/lib/Time/Hires.pm
+ Interface Style plain Functions, no references used
+ Language Used C and perl, a C compiler will be needed
+ Package Time-HiRes-1.65.tar.gz
+ Public License Unknown
+ Support Level Developer
+ Version Installed 1.52
+ Version on CPAN 1.65
+
+=cut
+
+sub details {
+ my $self = shift;
+ my $conf = $self->parent->configure_object();
+ my $cb = $self->parent;
+ my %hash = @_;
+
+ my $res = {
+ Author => loc("%1 (%2)", $self->author->author(),
+ $self->author->email() ),
+ Package => $self->package,
+ Description => $self->description || loc('None given'),
+ 'Version on CPAN' => $self->version,
+ };
+
+ ### check if we have the module installed
+ ### if so, add version have and version on cpan
+ $res->{'Version Installed'} = $self->installed_version
+ if $self->installed_version;
+ $res->{'Installed File'} = $self->installed_file if $self->installed_file;
+
+ my $i = 0;
+ for my $item( split '', $self->dslip ) {
+ $res->{ $cb->_dslip_defs->[$i]->[0] } =
+ $cb->_dslip_defs->[$i]->[1]->{$item} || loc('Unknown');
+ $i++;
+ }
+
+ return $res;
+}
+
+=head2 @list = $self->contains()
+
+Returns a list of module objects that represent the modules also
+present in the package of this module.
+
+For example, for C<Archive::Tar> this might return:
+
+ Archive::Tar
+ Archive::Tar::Constant
+ Archive::Tar::File
+
+=cut
+
+sub contains {
+ my $self = shift;
+ my $cb = $self->parent;
+ my $pkg = $self->package;
+
+ my @mods = $cb->search( type => 'package', allow => [qr/^$pkg$/] );
+
+ return @mods;
+}
+
+=pod
+
+=head2 @list_of_hrefs = $self->fetch_report()
+
+This function queries the CPAN testers database at
+I<http://testers.cpan.org/> for test results of specified module
+objects, module names or distributions.
+
+Look at L<CPANPLUS::Internals::Report::_query_report()> for details on
+the options you can pass and the return value to expect.
+
+=cut
+
+sub fetch_report {
+ my $self = shift;
+ my $cb = $self->parent;
+
+ return $cb->_query_report( @_, module => $self );
+}
+
+=pod
+
+=head2 $bool = $self->uninstall([type => [all|man|prog])
+
+This function uninstalls the specified module object.
+
+You can install 2 types of files, either C<man> pages or C<prog>ram
+files. Alternately you can specify C<all> to uninstall both (which
+is the default).
+
+Returns true on success and false on failure.
+
+Do note that this does an uninstall via the so-called C<.packlist>,
+so if you used a module installer like say, C<ports> or C<apt>, you
+should not use this, but use your package manager instead.
+
+=cut
+
+sub uninstall {
+ my $self = shift;
+ my $conf = $self->parent->configure_object();
+ my %hash = @_;
+
+ my ($type,$verbose);
+ my $tmpl = {
+ type => { default => 'all', allow => [qw|man prog all|],
+ store => \$type },
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ force => { default => $conf->get_conf('force') },
+ };
+
+ ### XXX add a warning here if your default install dist isn't
+ ### makefile or build -- that means you are using a package manager
+ ### and this will not do what you think!
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ if( $conf->get_conf('dist_type') and (
+ ($conf->get_conf('dist_type') ne INSTALLER_BUILD) or
+ ($conf->get_conf('dist_type') ne INSTALLER_MM))
+ ) {
+ msg(loc("You have a default installer type set (%1) ".
+ "-- you should probably use that package manager to " .
+ "uninstall modules", $conf->get_conf('dist_type')), $verbose);
+ }
+
+ ### check if we even have the module installed -- no point in continuing
+ ### otherwise
+ unless( $self->installed_version ) {
+ error( loc( "Module '%1' is not installed, so cannot uninstall",
+ $self->module ) );
+ return;
+ }
+
+ ### nothing to uninstall ###
+ my $files = $self->files( type => $type ) or return;
+ my $dirs = $self->directory_tree( type => $type ) or return;
+ my $sudo = $conf->get_program('sudo');
+
+ ### just in case there's no file; M::B doensn't provide .packlists yet ###
+ my $pack = $self->packlist;
+ $pack = $pack->[0]->packlist_file() if $pack;
+
+ ### first remove the files, then the dirs if they are empty ###
+ my $flag = 0;
+ for my $file( @$files, $pack ) {
+ next unless defined $file && -f $file;
+
+ msg(loc("Unlinking '%1'", $file), $verbose);
+
+ my @cmd = ($^X, "-eunlink+q[$file]");
+ unshift @cmd, $sudo if $sudo;
+
+ my $buffer;
+ unless ( run( command => \@cmd,
+ verbose => $verbose,
+ buffer => \$buffer )
+ ) {
+ error(loc("Failed to unlink '%1': '%2'",$file, $buffer));
+ $flag++;
+ }
+ }
+
+ for my $dir ( sort @$dirs ) {
+ local *DIR;
+ open DIR, $dir or next;
+ my @count = readdir(DIR);
+ close DIR;
+
+ next unless @count == 2; # . and ..
+
+ msg(loc("Removing '%1'", $dir), $verbose);
+
+ ### this fails on my win2k machines.. it indeed leaves the
+ ### dir, but it's not a critical error, since the files have
+ ### been removed. --kane
+ #unless( rmdir $dir ) {
+ # error( loc( "Could not remove '%1': %2", $dir, $! ) )
+ # unless $^O eq 'MSWin32';
+ #}
+
+ my @cmd = ($^X, "-ermdir+q[$dir]");
+ unshift @cmd, $sudo if $sudo;
+
+ my $buffer;
+ unless ( run( command => \@cmd,
+ verbose => $verbose,
+ buffer => \$buffer )
+ ) {
+ error(loc("Failed to rmdir '%1': %2",$dir,$buffer));
+ $flag++;
+ }
+ }
+
+ $self->status->uninstall(!$flag);
+ $self->status->installed( $flag ? 1 : undef);
+
+ return !$flag;
+}
+
+=pod
+
+=head2 @modobj = $self->distributions()
+
+Returns a list of module objects representing all releases for this
+module on success, false on failure.
+
+=cut
+
+sub distributions {
+ my $self = shift;
+ my %hash = @_;
+
+ my @list = $self->author->distributions( %hash, module => $self ) or return;
+
+ ### it's another release then by the same author ###
+ return grep { $_->package_name eq $self->package_name } @list;
+}
+
+=pod
+
+=head2 @list = $self->files ()
+
+Returns a list of files used by this module, if it is installed.
+
+=cut
+
+sub files {
+ return shift->_extutils_installed( @_, method => 'files' );
+}
+
+=pod
+
+=head2 @list = $self->directory_tree ()
+
+Returns a list of directories used by this module.
+
+=cut
+
+sub directory_tree {
+ return shift->_extutils_installed( @_, method => 'directory_tree' );
+}
+
+=pod
+
+=head2 @list = $self->packlist ()
+
+Returns the C<ExtUtils::Packlist> object for this module.
+
+=cut
+
+sub packlist {
+ return shift->_extutils_installed( @_, method => 'packlist' );
+}
+
+=pod
+
+=head2 @list = $self->validate ()
+
+Returns a list of files that are missing for this modules, but
+are present in the .packlist file.
+
+=cut
+
+sub validate {
+ return shift->_extutils_installed( method => 'validate' );
+}
+
+### generic method to call an ExtUtils::Installed method ###
+sub _extutils_installed {
+ my $self = shift;
+ my $conf = $self->parent->configure_object();
+ my %hash = @_;
+
+ my ($verbose,$type,$method);
+ my $tmpl = {
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose, },
+ type => { default => 'all',
+ allow => [qw|prog man all|],
+ store => \$type, },
+ method => { required => 1,
+ store => \$method,
+ allow => [qw|files directory_tree packlist
+ validate|],
+ },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### old versions of cygwin + perl < 5.8 are buggy here. bail out if we
+ ### find we're being used by them
+ { my $err = ON_OLD_CYGWIN;
+ if($err) { error($err); return };
+ }
+
+ return unless can_load(
+ modules => { 'ExtUtils::Installed' => '0.0' },
+ verbose => $verbose,
+ );
+
+ my $inst;
+ unless( $inst = ExtUtils::Installed->new() ) {
+ error( loc("Could not create an '%1' object", 'ExtUtils::Installed' ) );
+
+ ### in case it's being used directly... ###
+ return;
+ }
+
+
+ { ### EU::Installed can die =/
+ my @files;
+ eval { @files = $inst->$method( $self->module, $type ) };
+
+ if( $@ ) {
+ chomp $@;
+ error( loc("Could not get '%1' for '%2': %3",
+ $method, $self->module, $@ ) );
+ return;
+ }
+
+ return wantarray ? @files : \@files;
+ }
+}
+
+=head2 $bool = $self->add_to_includepath;
+
+Adds the current modules path to C<@INC> and C<$PERL5LIB>. This allows
+you to add the module from it's build dir to your path.
+
+You can reset C<@INC> and C<$PERL5LIB> to it's original state when you
+started the program, by calling:
+
+ $self->parent->flush('lib');
+
+=cut
+
+sub add_to_includepath {
+ my $self = shift;
+ my $cb = $self->parent;
+
+ if( my $dir = $self->status->extract ) {
+
+ $cb->_add_to_includepath(
+ directories => [
+ File::Spec->catdir(BLIB->($dir), LIB),
+ File::Spec->catdir(BLIB->($dir), ARCH),
+ BLIB->($dir),
+ ]
+ ) or return;
+
+ } else {
+ error(loc( "No extract dir registered for '%1' -- can not add ".
+ "add builddir to search path!", $self->module ));
+ return;
+ }
+
+ return 1;
+
+}
+
+=pod
+
+=head2 $path = $self->best_path_to_module_build();
+
+B<OBSOLETE>
+
+If a newer version of Module::Build is found in your path, it will
+return this C<special> path. If the newest version of C<Module::Build>
+is found in your regular C<@INC>, the method will return false. This
+indicates you do not need to add a special directory to your C<@INC>.
+
+Note that this is only relevant if you're building your own
+C<CPANPLUS::Dist::*> plugin -- the built-in dist types already have
+this taken care of.
+
+=cut
+
+### make sure we're always running 'perl Build.PL' and friends
+### against the highest version of module::build available
+sub best_path_to_module_build {
+ my $self = shift;
+
+ ### Since M::B will actually shell out and run the Build.PL, we must
+ ### make sure it refinds the proper version of M::B in the path.
+ ### that may be either in our cp::inc or in site_perl, or even a
+ ### new M::B being installed.
+ ### don't add anything else here, as that might screw up prereq checks
+
+ ### XXX this might be needed for Dist::MM too, if a makefile.pl is
+ ### masquerading as a Build.PL
+
+ ### did we find the most recent module::build in our installer path?
+
+ ### XXX can't do changes to @INC, they're being ignored by
+ ### new_from_context when writing a Build script. see ticket:
+ ### #8826 Module::Build ignores changes to @INC when writing Build
+ ### from new_from_context
+ ### XXX applied schwern's patches (as seen on CPANPLUS::Devel 10/12/04)
+ ### and upped the version to 0.26061 of the bundled version, and things
+ ### work again
+
+ ### this functionality is now obsolete -- prereqs should be installed
+ ### and we no longer use the CPANPLUS::inc magic.. so comment this out.
+# require Module::Build;
+# if( CPANPLUS::inc->path_to('Module::Build') and (
+# CPANPLUS::inc->path_to('Module::Build') eq
+# CPANPLUS::inc->installer_path )
+# ) {
+#
+# ### if the module being installed is *not* Module::Build
+# ### itself -- as that would undoubtedly be newer -- add
+# ### the path to the installers to @INC
+# ### if it IS module::build itself, add 'lib' to its path,
+# ### as the Build.PL would do as well, but the API doesn't.
+# ### this makes self updates possible
+# return $self->module eq 'Module::Build'
+# ? 'lib'
+# : CPANPLUS::inc->installer_path;
+# }
+
+ ### otherwise, the path was found through a 'normal' way of
+ ### scanning @INC.
+ return;
+}
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
+1;
+
+__END__
+
+todo:
+reports();
diff --git a/lib/CPANPLUS/Module/Author.pm b/lib/CPANPLUS/Module/Author.pm
new file mode 100644
index 0000000000..95de09cb7a
--- /dev/null
+++ b/lib/CPANPLUS/Module/Author.pm
@@ -0,0 +1,213 @@
+package CPANPLUS::Module::Author;
+
+use strict;
+
+use CPANPLUS::Error;
+use Params::Check qw[check];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+local $Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Module::Author
+
+=head1 SYNOPSIS
+
+ my $author = CPANPLUS::Module::Author->new(
+ author => 'Jack Ashton',
+ cpanid => 'JACKASH',
+ _id => INTERNALS_OBJECT_ID,
+ );
+
+ $author->cpanid;
+ $author->author;
+ $author->email;
+
+ @dists = $author->distributions;
+ @mods = $author->modules;
+
+ @accessors = CPANPLUS::Module::Author->accessors;
+
+=head1 DESCRIPTION
+
+C<CPANPLUS::Module::Author> creates objects from the information in the
+source files. These can then be used to query on.
+
+These objects should only be created internally. For C<fake> objects,
+there's the C<CPANPLUS::Module::Author::Fake> class.
+
+=head1 ACCESSORS
+
+An objects of this class has the following accessors:
+
+=over 4
+
+=item author
+
+Name of the author.
+
+=item cpanid
+
+The CPAN id of the author.
+
+=item email
+
+The email address of the author, which defaults to '' if not provided.
+
+=item parent
+
+The C<CPANPLUS::Internals::Object> that spawned this module object.
+
+=back
+
+=cut
+
+my $tmpl = {
+ author => { required => 1 }, # full name of the author
+ cpanid => { required => 1 }, # cpan id
+ email => { default => '' }, # email address of the author
+ _id => { required => 1 }, # id of the Internals object that spawned us
+};
+
+### autogenerate accessors ###
+for my $key ( keys %$tmpl ) {
+ no strict 'refs';
+ *{__PACKAGE__."::$key"} = sub {
+ my $self = shift;
+ $self->{$key} = $_[0] if @_;
+ return $self->{$key};
+ }
+}
+
+sub parent {
+ my $self = shift;
+ my $obj = CPANPLUS::Internals->_retrieve_id( $self->_id );
+
+ return $obj;
+}
+
+=pod
+
+=head1 METHODS
+
+=head2 $auth = CPANPLUS::Module::Author->new( author => AUTHOR_NAME, cpanid => CPAN_ID, _id => INTERNALS_ID [, email => AUTHOR_EMAIL] )
+
+This method returns a C<CPANPLUS::Module::Author> object, based on the given
+parameters.
+
+Returns false on failure.
+
+=cut
+
+sub new {
+ my $class = shift;
+ my %hash = @_;
+
+ ### don't check the template for sanity
+ ### -- we know it's good and saves a lot of performance
+ local $Params::Check::SANITY_CHECK_TEMPLATE = 0;
+
+ my $object = check( $tmpl, \%hash ) or return;
+
+ return bless $object, $class;
+}
+
+=pod
+
+=head2 @mod_objs = $auth->modules()
+
+Return a list of module objects this author has released.
+
+=cut
+
+sub modules {
+ my $self = shift;
+ my $cb = $self->parent;
+
+ my $aref = $cb->_search_module_tree(
+ type => 'author',
+ allow => [$self],
+ );
+ return @$aref if $aref;
+ return;
+}
+
+=pod
+
+=head2 @dists = $auth->distributions()
+
+Returns a list of module objects representing all the distributions
+this author has released.
+
+=cut
+
+sub distributions {
+ my $self = shift;
+ my %hash = @_;
+
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+ local $Params::Check::NO_DUPLICATES = 1;
+
+ my $mod;
+ my $tmpl = {
+ module => { default => '', store => \$mod },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ ### if we didn't get a module object passed, we'll find one ourselves ###
+ unless( $mod ) {
+ my @list = $self->modules;
+ if( @list ) {
+ $mod = $list[0];
+ } else {
+ error( loc( "This author has released no modules" ) );
+ return;
+ }
+ }
+
+ my $file = $mod->checksums( %hash );
+ my $href = $mod->_parse_checksums_file( file => $file ) or return;
+
+ my @rv;
+ for my $dist ( keys %$href ) {
+ my $clone = $mod->clone;
+
+ $clone->package( $dist );
+ $clone->module( $clone->package_name );
+ $clone->version( $clone->package_version );
+ $clone->mtime( $href->{$dist}->{'mtime'} ); # release date
+
+ ### .meta files are now also in the checksums file,
+ ### which means we have to filter out things that dont
+ ### match our regex
+ push @rv, $clone if $clone->package_extension;
+ }
+
+ return @rv;
+}
+
+
+=pod
+
+=head1 CLASS METHODS
+
+=head2 accessors ()
+
+Returns a list of all accessor methods to the object
+
+=cut
+
+sub accessors { return keys %$tmpl };
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Module/Author/Fake.pm b/lib/CPANPLUS/Module/Author/Fake.pm
new file mode 100644
index 0000000000..115c29ed7b
--- /dev/null
+++ b/lib/CPANPLUS/Module/Author/Fake.pm
@@ -0,0 +1,80 @@
+package CPANPLUS::Module::Author::Fake;
+
+
+use CPANPLUS::Module::Author;
+use CPANPLUS::Internals;
+use CPANPLUS::Error;
+
+use strict;
+use vars qw[@ISA];
+use Params::Check qw[check];
+
+@ISA = qw[CPANPLUS::Module::Author];
+
+$Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Module::Author::Fake
+
+=head1 SYNOPSIS
+
+ my $auth = CPANPLUS::Module::Author::Fake->new(
+ name => 'Foo Bar',
+ email => 'luser@foo.com',
+ cpanid => 'FOO',
+ _id => $cpan->id,
+ );
+
+=head1 DESCRIPTION
+
+A class for creating fake author objects, for shortcut use internally
+by CPANPLUS.
+
+Inherits from C<CPANPLUS::Module::Author>.
+
+=head1 METHODS
+
+=head2 new( _id => DIGIT )
+
+Creates a dummy author object. It can take the same options as
+C<< CPANPLUS::Module::Author->new >>, but will fill in default ones
+if none are provided. Only the _id key is required.
+
+=cut
+
+sub new {
+ my $class = shift;
+ my %hash = @_;
+
+ my $tmpl = {
+ author => { default => 'CPANPLUS Internals' },
+ email => { default => 'cpanplus-info@lists.sf.net' },
+ cpanid => { default => 'CPANPLUS' },
+ _id => { default => CPANPLUS::Internals->_last_id },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ my $obj = CPANPLUS::Module::Author->new( %$args ) or return;
+
+ unless( $obj->_id ) {
+ error(loc("No '%1' specified -- No CPANPLUS object associated!",'_id'));
+ return;
+ }
+
+ ### rebless object ###
+ return bless $obj, $class;
+}
+
+1;
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Module/Checksums.pm b/lib/CPANPLUS/Module/Checksums.pm
new file mode 100644
index 0000000000..92a2cc27f3
--- /dev/null
+++ b/lib/CPANPLUS/Module/Checksums.pm
@@ -0,0 +1,251 @@
+package CPANPLUS::Module::Checksums;
+
+use strict;
+use vars qw[@ISA];
+
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+use FileHandle;
+
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load];
+
+$Params::Check::VERBOSE = 1;
+
+@ISA = qw[ CPANPLUS::Module::Signature ];
+
+=head1 NAME
+
+CPANPLUS::Module::Checksums
+
+=head1 SYNOPSIS
+
+ $file = $modobj->checksums;
+ $bool = $mobobj->_validate_checksum;
+
+=head1 DESCRIPTION
+
+This is a class that provides functions for checking the checksum
+of a distribution. Should not be loaded directly, but used via the
+interface provided via C<CPANPLUS::Module>.
+
+=head1 METHODS
+
+=head2 $mod->checksums
+
+Fetches the checksums file for this module object.
+For the options it can take, see C<CPANPLUS::Module::fetch()>.
+
+Returns the location of the checksums file on success and false
+on error.
+
+The location of the checksums file is also stored as
+
+ $mod->status->checksums
+
+=cut
+
+sub checksums {
+ my $mod = shift or return;
+
+ my $file = $mod->_get_checksums_file( @_ );
+
+ return $mod->status->checksums( $file ) if $file;
+
+ return;
+}
+
+### checks if the package checksum matches the one
+### from the checksums file
+sub _validate_checksum {
+ my $self = shift; #must be isa CPANPLUS::Module
+ my $conf = $self->parent->configure_object;
+ my %hash = @_;
+
+ my $verbose;
+ my $tmpl = {
+ verbose => { default => $conf->get_conf('verbose'),
+ store => \$verbose },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### if we can't check it, we must assume it's ok ###
+ return $self->status->checksum_ok(1)
+ unless can_load( modules => { 'Digest::MD5' => '0.0' } );
+ #class CPANPLUS::Module::Status is runtime-generated
+
+ my $file = $self->_get_checksums_file( verbose => $verbose ) or (
+ error(loc(q[Could not fetch '%1' file], CHECKSUMS)), return );
+
+ $self->_check_signature_for_checksum_file( file => $file ) or (
+ error(loc(q[Could not verify '%1' file], CHECKSUMS)), return );
+ #for whole CHECKSUMS file
+
+ my $href = $self->_parse_checksums_file( file => $file ) or (
+ error(loc(q[Could not parse '%1' file], CHECKSUMS)), return );
+
+ my $size = $href->{ $self->package }->{'size'};
+
+ ### the checksums file tells us the size of the archive
+ ### but the downloaded file is of different size
+ if( defined $size ) {
+ if( not (-s $self->status->fetch == $size) ) {
+ error(loc( "Archive size does not match for '%1': " .
+ "size is '%2' but should be '%3'",
+ $self->package, -s $self->status->fetch, $size));
+ return $self->status->checksum_ok(0);
+ }
+ } else {
+ msg(loc("Archive size is not known for '%1'",$self->package),$verbose);
+ }
+
+ my $md5 = $href->{ $self->package }->{'md5'};
+
+ unless( defined $md5 ) {
+ msg(loc("No 'md5' checksum known for '%1'",$self->package),$verbose);
+
+ return $self->status->checksum_ok(1);
+ }
+
+ $self->status->checksum_value($md5);
+
+
+ my $fh = FileHandle->new( $self->status->fetch ) or return;
+ binmode $fh;
+
+ my $ctx = Digest::MD5->new;
+ $ctx->addfile( $fh );
+
+ my $flag = $ctx->hexdigest eq $md5;
+ $flag
+ ? msg(loc("Checksum matches for '%1'", $self->package),$verbose)
+ : error(loc("Checksum does not match for '%1': " .
+ "MD5 is '%2' but should be '%3'",
+ $self->package, $ctx->hexdigest, $md5),$verbose);
+
+
+ return $self->status->checksum_ok(1) if $flag;
+ return $self->status->checksum_ok(0);
+}
+
+
+### fetches the module objects checksum file ###
+sub _get_checksums_file {
+ my $self = shift;
+ my %hash = @_;
+
+ my $clone = $self->clone;
+ $clone->package( CHECKSUMS );
+
+ my $file = $clone->fetch( %hash, force => 1 ) or return;
+
+ return $file;
+}
+
+sub _parse_checksums_file {
+ my $self = shift;
+ my %hash = @_;
+
+ my $file;
+ my $tmpl = {
+ file => { required => 1, allow => FILE_READABLE, store => \$file },
+ };
+ my $args = check( $tmpl, \%hash );
+
+ my $fh = OPEN_FILE->( $file ) or return;
+
+ ### loop over the header, there might be a pgp signature ###
+ my $signed;
+ while (<$fh>) {
+ last if /^\$cksum = \{\s*$/; # skip till this line
+ my $header = PGP_HEADER; # but be tolerant of whitespace
+ $signed = 1 if /^${header}\s*$/;# due to crossplatform linebreaks
+ }
+
+ ### read the filehandle, parse it rather than eval it, even though it
+ ### *should* be valid perl code
+ my $dist;
+ my $cksum = {};
+ while (<$fh>) {
+
+ if (/^\s*'([^']+)' => \{\s*$/) {
+ $dist = $1;
+
+ } elsif (/^\s*'([^']+)' => '?([^'\n]+)'?,?\s*$/ and defined $dist) {
+ $cksum->{$dist}{$1} = $2;
+
+ } elsif (/^\s*}[,;]?\s*$/) {
+ undef $dist;
+
+ } elsif (/^__END__\s*$/) {
+ last;
+
+ } else {
+ error( loc("Malformed %1 line: %2", CHECKSUMS, $_) );
+ }
+ }
+
+ return $cksum;
+}
+
+sub _check_signature_for_checksum_file {
+ my $self = shift;
+
+ my $conf = $self->parent->configure_object;
+ my %hash = @_;
+
+ ### you don't want to check signatures,
+ ### so let's just return true;
+ return 1 unless $conf->get_conf('signature');
+
+ my($force,$file,$verbose);
+ my $tmpl = {
+ file => { required => 1, allow => FILE_READABLE, store => \$file },
+ force => { default => $conf->get_conf('force'), store => \$force },
+ verbose => { default => $conf->get_conf('verbose'), store => \$verbose },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ my $fh = OPEN_FILE->($file) or return;
+
+ my $signed;
+ while (<$fh>) {
+ my $header = PGP_HEADER;
+ $signed = 1 if /^$header$/;
+ }
+
+ if ( !$signed ) {
+ msg(loc("No signature found in %1 file '%2'",
+ CHECKSUMS, $file), $verbose);
+
+ return 1 unless $force;
+
+ error( loc( "%1 file '%2' is not signed -- aborting",
+ CHECKSUMS, $file ) );
+ return;
+
+ }
+
+ if( can_load( modules => { 'Module::Signature' => '0.06' } ) ) {
+ # local $Module::Signature::SIGNATURE = $file;
+ # ... check signatures ...
+ }
+
+ return 1;
+}
+
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
+1;
diff --git a/lib/CPANPLUS/Module/Fake.pm b/lib/CPANPLUS/Module/Fake.pm
new file mode 100644
index 0000000000..84d0233cf8
--- /dev/null
+++ b/lib/CPANPLUS/Module/Fake.pm
@@ -0,0 +1,86 @@
+package CPANPLUS::Module::Fake;
+
+
+use CPANPLUS::Error;
+use CPANPLUS::Module;
+use CPANPLUS::Module::Author::Fake;
+use CPANPLUS::Internals;
+
+use strict;
+use vars qw[@ISA];
+use Params::Check qw[check];
+
+@ISA = qw[CPANPLUS::Module];
+$Params::Check::VERBOSE = 1;
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Module::Fake
+
+=head1 SYNOPSIS
+
+ my $obj = CPANPLUS::Module::Fake->new(
+ module => 'Foo',
+ path => 'ftp/path/to/foo',
+ author => CPANPLUS::Module::Author::Fake->new,
+ package => 'fake-1.1.tgz',
+ _id => $cpan->_id,
+ );
+
+=head1 DESCRIPTION
+
+A class for creating fake module objects, for shortcut use internally
+by CPANPLUS.
+
+Inherits from C<CPANPLUS::Module>.
+
+=head1 METHODS
+
+=head2 new( module => $mod, path => $path, package => $pkg, [_id => DIGIT] )
+
+Creates a dummy module object from the above parameters. It can
+take more options (same as C<< CPANPLUS::Module->new >> but the above
+are required.
+
+=cut
+
+sub new {
+ my $class = shift;
+ my %hash = @_;
+
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ module => { required => 1 },
+ path => { required => 1 },
+ package => { required => 1 },
+ _id => { default => CPANPLUS::Internals->_last_id },
+ author => { default => '' },
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+
+ $args->{author} ||= CPANPLUS::Module::Author::Fake->new(
+ _id => $args->{_id} );
+
+ my $obj = CPANPLUS::Module->new( %$args ) or return;
+
+ unless( $obj->_id ) {
+ error(loc("No '%1' specified -- No CPANPLUS object associated!",'_id'));
+ return;
+ }
+
+ ### rebless object ###
+ return bless $obj, $class;
+}
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Module/Signature.pm b/lib/CPANPLUS/Module/Signature.pm
new file mode 100644
index 0000000000..cec6f2906b
--- /dev/null
+++ b/lib/CPANPLUS/Module/Signature.pm
@@ -0,0 +1,65 @@
+package CPANPLUS::Module::Signature;
+
+use strict;
+
+
+use Cwd;
+use CPANPLUS::Error;
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load];
+
+
+### detached sig, not actually used afaik --kane ###
+#sub get_signature {
+# my $self = shift;
+#
+# my $clone = $self->clone;
+# $clone->package( $self->package . '.sig' );
+#
+# return $clone->fetch;
+#}
+
+sub check_signature {
+ my $self = shift;
+ my $cb = $self->parent;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $verbose;
+ my $tmpl = {
+ verbose => {default => $conf->get_conf('verbose'), store => \$verbose},
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $dir = $self->status->extract or (
+ error( loc( "Do not know what dir '%1' was extracted to; ".
+ "Cannot check signature", $self->module ) ),
+ return );
+
+ my $cwd = cwd();
+ unless( $cb->_chdir( dir => $dir ) ) {
+ error(loc( "Could not chdir to '%1', cannot verify distribution '%2'",
+ $dir, $self->module ));
+ return;
+ }
+
+
+ ### check prerequisites
+ my $flag;
+ my $use_list = { 'Module::Signature' => '0.06' };
+ if( can_load( modules => $use_list, verbose => 1 ) ) {
+ my $rv = Module::Signature::verify();
+
+ unless ($rv eq Module::Signature::SIGNATURE_OK() or
+ $rv eq Module::Signature::SIGNATURE_MISSING()
+ ) {
+ $flag++; # whoops, bad sig
+ }
+ }
+
+ $cb->_chdir( dir => $cwd );
+ return $flag ? 0 : 1;
+}
+
+1;
diff --git a/lib/CPANPLUS/Selfupdate.pm b/lib/CPANPLUS/Selfupdate.pm
new file mode 100644
index 0000000000..2271dd4b17
--- /dev/null
+++ b/lib/CPANPLUS/Selfupdate.pm
@@ -0,0 +1,447 @@
+package CPANPLUS::Selfupdate;
+
+use strict;
+use Params::Check qw[check];
+use IPC::Cmd qw[can_run];
+use CPANPLUS::Error qw[error msg];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+use CPANPLUS::Internals::Constants;
+
+$Params::Check::VERBOSE = 1;
+
+=head1 NAME
+
+CPANPLUS::Selfupdate
+
+=head1 SYNOPSIS
+
+ $su = $cb->selfupdate_object;
+
+ @feats = $su->list_features;
+ @feats = $su->list_enabled_features;
+
+ @mods = map { $su->modules_for_feature( $_ ) } @feats;
+ @mods = $su->list_core_dependencies;
+ @mods = $su->list_core_modules;
+
+ for ( @mods ) {
+ print $_->name " should be version " . $_->version_required;
+ print "Installed version is not uptodate!"
+ unless $_->is_installed_version_sufficient;
+ }
+
+ $ok = $su->selfupdate( update => 'all', latest => 0 );
+
+=cut
+
+### a config has describing our deps etc
+{
+
+ my $Modules = {
+ dependencies => {
+ 'File::Fetch' => '0.08', # win32 ftp support
+ 'File::Spec' => '0.82',
+ 'IPC::Cmd' => '0.36', # 5.6.2 compat: 2-arg open
+ 'Locale::Maketext::Simple' => '0.01',
+ 'Log::Message' => '0.01',
+ 'Module::Load' => '0.10',
+ 'Module::Load::Conditional' => '0.16', # Better parsing: #23995
+ 'version' => '0.70', # needed for M::L::C
+ # addresses #24630 and
+ # #24675
+ 'Params::Check' => '0.22',
+ 'Package::Constants' => '0.01',
+ 'Term::UI' => '0.05',
+ 'Test::Harness' => '2.62', # due to bug #19505
+ # only 2.58 and 2.60 are bad
+ 'Test::More' => '0.47', # to run our tests
+ 'Archive::Extract' => '0.16', # ./Dir bug fix
+ 'Archive::Tar' => '1.23',
+ 'IO::Zlib' => '1.04', # needed for Archive::Tar
+ 'Object::Accessor' => '0.32', # overloaded stringification
+ 'Module::CoreList' => '2.09',
+ 'Module::Pluggable' => '2.4',
+ 'Module::Loaded' => '0.01',
+ },
+
+ features => {
+ # config_key_name => [
+ # sub { } to list module key/value pairs
+ # sub { } to check if feature is enabled
+ # ]
+ prefer_makefile => [
+ sub {
+ my $cb = shift;
+ $cb->configure_object->get_conf('prefer_makefile')
+ ? { }
+ : { 'CPANPLUS::Dist::Build' => '0.04' };
+ },
+ sub { return 1 }, # always enabled
+ ],
+ cpantest => [
+ {
+ LWP => '0.0',
+ 'LWP::UserAgent' => '0.0',
+ 'HTTP::Request' => '0.0',
+ URI => '0.0',
+ YAML => '0.0',
+ 'Test::Reporter' => 1.27,
+ },
+ sub {
+ my $cb = shift;
+ return $cb->configure_object->get_conf('cpantest');
+ },
+ ],
+ dist_type => [
+ sub {
+ my $cb = shift;
+ my $dist = $cb->configure_object->get_conf('dist_type');
+ return { $dist => '0.0' } if $dist;
+ return;
+ },
+ sub {
+ my $cb = shift;
+ return $cb->configure_object->get_conf('dist_type');
+ },
+ ],
+
+ md5 => [
+ {
+ 'Digest::MD5' => '0.0',
+ },
+ sub {
+ my $cb = shift;
+ return $cb->configure_object->get_conf('md5');
+ },
+ ],
+ shell => [
+ sub {
+ my $cb = shift;
+ my $dist = $cb->configure_object->get_conf('shell');
+ return { $dist => '0.0' } if $dist;
+ return;
+ },
+ sub { return 1 },
+ ],
+ signature => [
+ sub {
+ my $cb = shift;
+ return if can_run('gpg') and
+ $cb->configure_object->get_conf('prefer_bin');
+ return { 'Crypt::OpenPGP' => '0.0' };
+ },
+ sub {
+ my $cb = shift;
+ return $cb->configure_object->get_conf('signature');
+ },
+ ],
+ storable => [
+ { 'Storable' => '0.0' },
+ sub {
+ my $cb = shift;
+ return $cb->configure_object->get_conf('storable');
+ },
+ ],
+ },
+ core => {
+ 'CPANPLUS' => '0.0',
+ },
+ };
+
+ sub _get_config { return $Modules }
+}
+
+=head1 METHODS
+
+=head2 $self = CPANPLUS::Selfupdate->new( $backend_object );
+
+Sets up a new selfupdate object. Called automatically when
+a new backend object is created.
+
+=cut
+
+sub new {
+ my $class = shift;
+ my $cb = shift or return;
+ return bless sub { $cb }, $class;
+}
+
+
+
+=head2 $bool = $self->selfupdate( update => "core|dependencies|enabled_features|features|all", latest => BOOL )
+
+Selfupdate CPANPLUS. You can update either the core (CPANPLUS itself),
+the core dependencies, all features you have currently turned on, or
+all features available, or everything.
+
+The C<latest> option determines whether it should update to the latest
+version on CPAN, or if the minimal required version for CPANPLUS is
+good enough.
+
+Returns true on success, false on error.
+
+=cut
+
+sub selfupdate {
+ my $self = shift;
+ my $cb = $self->();
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ ### cache to find the relevant modules
+ my $cache = {
+ core => sub { $self->list_core_modules },
+ dependencies => sub { $self->list_core_dependencies },
+ enabled_features => sub { map { $self->modules_for_feature( $_ ) }
+ $self->list_enabled_features
+ },
+ features => sub { map { $self->modules_for_feature( $_ ) }
+ $self->list_features
+ },
+ ### make sure to do 'core' first, in case
+ ### we are out of date ourselves
+ all => [ qw|core dependencies enabled_features| ],
+ };
+
+ my($type, $latest, $force);
+ my $tmpl = {
+ update => { required => 1, store => \$type,
+ allow => [ keys %$cache ], },
+ latest => { default => 0, store => \$latest, allow => BOOLEANS },
+ force => { default => $conf->get_conf('force'), store => \$force },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $ref = $cache->{$type};
+ my @mods = UNIVERSAL::isa( $ref, 'ARRAY' )
+ ? map { $cache->{$_}->() } @$ref
+ : $ref->();
+
+ ### do we need the latest versions?
+ @mods = $latest
+ ? @mods
+ : grep { $_->is_installed_version_sufficient } @mods;
+
+ my $flag;
+ for my $mod ( @mods ) {
+ unless( $mod->install( force => $force ) ) {
+ $flag++;
+ error(loc("Failed to update module '%1'", $mod->name));
+ }
+ }
+
+ return if $flag;
+ return 1;
+}
+
+=head2 @features = $self->list_features
+
+Returns a list of features that are supported by CPANPLUS.
+
+=cut
+
+sub list_features {
+ my $self = shift;
+ return keys %{ $self->_get_config->{'features'} };
+}
+
+=head2 @features = $self->list_enabled_features
+
+Returns a list of features that are enabled in your current
+CPANPLUS installation.
+
+=cut
+
+sub list_enabled_features {
+ my $self = shift;
+ my $cb = $self->();
+
+ my @enabled;
+ for my $feat ( $self->list_features ) {
+ my $ref = $self->_get_config->{'features'}->{$feat}->[1];
+ push @enabled, $feat if $ref->($cb);
+ }
+
+ return @enabled;
+}
+
+=head2 @mods = $self->modules_for_feature( FEATURE [,AS_HASH] )
+
+Returns a list of C<CPANPLUS::Selfupdate::Module> objects which
+represent the modules required to support this feature.
+
+For a list of features, call the C<list_features> method.
+
+If the C<AS_HASH> argument is provided, no module objects are
+returned, but a hashref where the keys are names of the modules,
+and values are their minimum versions.
+
+=cut
+
+sub modules_for_feature {
+ my $self = shift;
+ my $feature = shift or return;
+ my $as_hash = shift || 0;
+ my $cb = $self->();
+
+ unless( exists $self->_get_config->{'features'}->{$feature} ) {
+ error(loc("Unknown feature '%1'", $feature));
+ return;
+ }
+
+ my $ref = $self->_get_config->{'features'}->{$feature}->[0];
+
+ ### it's either a list of modules/versions or a subroutine that
+ ### returns a list of modules/versions
+ my $href = UNIVERSAL::isa( $ref, 'HASH' ) ? $ref : $ref->( $cb );
+
+ return unless $href; # nothing needed for the feature?
+
+ return $href if $as_hash;
+ return $self->_hashref_to_module( $href );
+}
+
+
+=head2 @mods = $self->list_core_dependencies( [AS_HASH] )
+
+Returns a list of C<CPANPLUS::Selfupdate::Module> objects which
+represent the modules that comprise the core dependencies of CPANPLUS.
+
+If the C<AS_HASH> argument is provided, no module objects are
+returned, but a hashref where the keys are names of the modules,
+and values are their minimum versions.
+
+=cut
+
+sub list_core_dependencies {
+ my $self = shift;
+ my $as_hash = shift || 0;
+ my $cb = $self->();
+ my $href = $self->_get_config->{'dependencies'};
+
+ return $href if $as_hash;
+ return $self->_hashref_to_module( $href );
+}
+
+=head2 @mods = $self->list_core_modules( [AS_HASH] )
+
+Returns a list of C<CPANPLUS::Selfupdate::Module> objects which
+represent the modules that comprise the core of CPANPLUS.
+
+If the C<AS_HASH> argument is provided, no module objects are
+returned, but a hashref where the keys are names of the modules,
+and values are their minimum versions.
+
+=cut
+
+sub list_core_modules {
+ my $self = shift;
+ my $as_hash = shift || 0;
+ my $cb = $self->();
+ my $href = $self->_get_config->{'core'};
+
+ return $href if $as_hash;
+ return $self->_hashref_to_module( $href );
+}
+
+sub _hashref_to_module {
+ my $self = shift;
+ my $cb = $self->();
+ my $href = shift or return;
+
+ return map {
+ CPANPLUS::Selfupdate::Module->new(
+ $cb->module_tree($_) => $href->{$_}
+ )
+ } keys %$href;
+}
+
+
+=head1 CPANPLUS::Selfupdate::Module
+
+C<CPANPLUS::Selfupdate::Module> extends C<CPANPLUS::Module> objects
+by providing accessors to aid in selfupdating CPANPLUS.
+
+These objects are returned by all methods of C<CPANPLUS::Selfupdate>
+that return module objects.
+
+=cut
+
+{ package CPANPLUS::Selfupdate::Module;
+ use base 'CPANPLUS::Module';
+
+ ### stores module name -> cpanplus required version
+ ### XXX only can deal with 1 pair!
+ my %Cache = ();
+ my $Acc = 'version_required';
+
+ sub new {
+ my $class = shift;
+ my $mod = shift or return;
+ my $ver = shift; return unless defined $ver;
+
+ my $obj = $mod->clone; # clone the module object
+ bless $obj, $class; # rebless it to our class
+
+ $obj->$Acc( $ver );
+
+ return $obj;
+ }
+
+=head2 $version = $mod->version_required
+
+Returns the version of this module required for CPANPLUS.
+
+=cut
+
+ sub version_required {
+ my $self = shift;
+ $Cache{ $self->name } = shift() if @_;
+ return $Cache{ $self->name };
+ }
+
+=head2 $bool = $mod->is_installed_version_sufficient
+
+Returns true if the installed version of this module is sufficient
+for CPANPLUS, or false if it is not.
+
+=cut
+
+
+ sub is_installed_version_sufficient {
+ my $self = shift;
+ return $self->is_uptodate( version => $self->$Acc );
+ }
+
+}
+
+1;
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Shell.pm b/lib/CPANPLUS/Shell.pm
new file mode 100644
index 0000000000..4128e03c95
--- /dev/null
+++ b/lib/CPANPLUS/Shell.pm
@@ -0,0 +1,314 @@
+package CPANPLUS::Shell;
+
+use strict;
+
+use CPANPLUS::Error;
+use CPANPLUS::Configure;
+
+
+use Module::Load qw[load];
+use Params::Check qw[check];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+$Params::Check::VERBOSE = 1;
+
+use vars qw[@ISA $SHELL $DEFAULT];
+
+$DEFAULT = 'CPANPLUS::Shell::Default';
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Shell
+
+=head1 SYNOPSIS
+
+ use CPANPLUS::Shell; # load the shell indicated by your
+ # config -- defaults to
+ # CPANPLUS::Shell::Default
+
+ use CPANPLUS::Shell qw[Classic] # load CPANPLUS::Shell::Classic;
+
+ my $ui = CPANPLUS::Shell->new();
+ my $name = $ui->which; # Find out what shell you loaded
+
+ $ui->shell; # run the ui shell
+
+
+=head1 DESCRIPTION
+
+This module is the generic loading (and base class) for all C<CPANPLUS>
+shells. Through this module you can load any installed C<CPANPLUS>
+shell.
+
+Just about all the functionality is provided by the shell that you have
+loaded, and not by this class (which merely functions as a generic
+loading class), so please consult the documentation of your shell of
+choice.
+
+=cut
+
+
+sub import {
+ my $class = shift;
+ my $option = shift;
+ ### XXX this should offer to reconfigure CPANPLUS, somehow. --rs
+ my $conf = CPANPLUS::Configure->new()
+ or die loc("No configuration available -- aborting") . $/;
+
+ ### find out what shell we're supposed to load ###
+ $SHELL = $option
+ ? $class . '::' . $option
+ : $conf->get_conf('shell') || $DEFAULT;
+
+ ### load the shell, fall back to the default if required
+ ### and die if even that doesn't work
+ EVAL: {
+ eval { load $SHELL };
+
+ if( $@ ) {
+ my $err = $@;
+
+ die loc("Your default shell '%1' is not available: %2",
+ $DEFAULT, $err) .
+ loc("Check your installation!") . "\n"
+ if $SHELL eq $DEFAULT;
+
+ warn loc("Failed to use '%1': %2", $SHELL, $err),
+ loc("Switching back to the default shell '%1'", $DEFAULT),
+ "\n";
+
+ $SHELL = $DEFAULT;
+ redo EVAL;
+ }
+ }
+ @ISA = ($SHELL);
+}
+
+sub which { return $SHELL }
+
+1;
+
+###########################################################################
+### abstracted out subroutines available to programmers of other shells ###
+###########################################################################
+
+package CPANPLUS::Shell::_Base::ReadLine;
+
+use strict;
+use vars qw($AUTOLOAD $TMPL);
+
+use FileHandle;
+use CPANPLUS::Error;
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+$Params::Check::VERBOSE = 1;
+
+
+$TMPL = {
+ brand => { default => '', strict_type => 1 },
+ prompt => { default => '> ', strict_type => 1 },
+ pager => { default => '' },
+ backend => { default => '' },
+ term => { default => '' },
+ format => { default => '' },
+ dist_format => { default => '' },
+ remote => { default => undef },
+ noninteractive => { default => '' },
+ cache => { default => [ ] },
+ _old_sigpipe => { default => '', no_override => 1 },
+ _old_outfh => { default => '', no_override => 1 },
+ _signals => { default => { INT => { } }, no_override => 1 },
+};
+
+### autogenerate accessors ###
+for my $key ( keys %$TMPL ) {
+ no strict 'refs';
+ *{__PACKAGE__."::$key"} = sub {
+ my $self = shift;
+ $self->{$key} = $_[0] if @_;
+ return $self->{$key};
+ }
+}
+
+sub _init {
+ my $class = shift;
+ my %hash = @_;
+
+ my $self = check( $TMPL, \%hash ) or return;
+
+ bless $self, $class;
+
+ ### signal handler ###
+ $SIG{INT} = $self->_signals->{INT}->{handler} =
+ sub {
+ unless ( $self->_signals->{INT}->{count}++ ) {
+ warn loc("Caught SIGINT"), "\n";
+ } else {
+ warn loc("Got another SIGINT"), "\n"; die;
+ }
+ };
+ ### end sig handler ###
+
+ return $self;
+}
+
+### display shell's banner, takes the Backend object as argument
+sub _show_banner {
+ my $self = shift;
+ my $cpan = $self->backend;
+ my $term = $self->term;
+
+ ### Tries to probe for our ReadLine support status
+ # a) under an interactive shell?
+ my $rl_avail = (!$term->isa('CPANPLUS::Shell::_Faked'))
+ # b) do we have a tty terminal?
+ ? (-t STDIN)
+ # c) should we enable the term?
+ ? (!$self->__is_bad_terminal($term))
+ # d) external modules available?
+ ? ($term->ReadLine ne "Term::ReadLine::Stub")
+ # a+b+c+d => "Smart" terminal
+ ? loc("enabled")
+ # a+b+c => "Stub" terminal
+ : loc("available (try 'i Term::ReadLine::Perl')")
+ # a+b => "Bad" terminal
+ : loc("disabled")
+ # a => "Dumb" terminal
+ : loc("suppressed")
+ # none => "Faked" terminal
+ : loc("suppressed in batch mode");
+
+ $rl_avail = loc("ReadLine support %1.", $rl_avail);
+ $rl_avail = "\n*** $rl_avail" if (length($rl_avail) > 45);
+
+ print loc("%1 -- CPAN exploration and module installation (v%2)",
+ $self->which, $self->which->VERSION()), "\n",
+ loc("*** Please report bugs to <bug-cpanplus\@rt.cpan.org>."), "\n",
+ loc("*** Using CPANPLUS::Backend v%1. %2",
+ $cpan->VERSION, $rl_avail), "\n\n";
+}
+
+### checks whether the Term::ReadLine is broken and needs to fallback to Stub
+sub __is_bad_terminal {
+ my $self = shift;
+ my $term = $self->term;
+
+ return unless $^O eq 'MSWin32';
+
+ ### replace the term with the default (stub) one
+ return $self->term(Term::ReadLine::Stub->new( $self->brand ) );
+}
+
+### open a pager handle
+sub _pager_open {
+ my $self = shift;
+ my $cpan = $self->backend;
+ my $cmd = $cpan->configure_object->get_program('pager') or return;
+
+ $self->_old_sigpipe( $SIG{PIPE} );
+ $SIG{PIPE} = 'IGNORE';
+
+ my $fh = new FileHandle;
+ unless ( $fh->open("| $cmd") ) {
+ error(loc("could not pipe to %1: %2\n", $cmd, $!) );
+ return;
+ }
+
+ $fh->autoflush(1);
+
+ $self->pager( $fh );
+ $self->_old_outfh( select $fh );
+
+ return $fh;
+}
+
+### print to the current pager handle, or STDOUT if it's not opened
+sub _pager_close {
+ my $self = shift;
+ my $pager = $self->pager or return;
+
+ $pager->close if (ref($pager) and $pager->can('close'));
+
+ $self->pager( undef );
+
+ select $self->_old_outfh;
+ $SIG{PIPE} = $self->_old_sigpipe;
+
+ return 1;
+}
+
+
+
+{
+ my $win32_console;
+
+ ### determines row count of current terminal; defaults to 25.
+ ### used by the pager functions
+ sub _term_rowcount {
+ my $self = shift;
+ my $cpan = $self->backend;
+ my %hash = @_;
+
+ my $default;
+ my $tmpl = {
+ default => { default => 25, allow => qr/^\d$/,
+ store => \$default }
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ if ( $^O eq 'MSWin32' ) {
+ if ( can_load( modules => { 'Win32::Console' => '0.0' } ) ) {
+ $win32_console ||= Win32::Console->new();
+ my $rows = ($win32_console->Info)[-1];
+ return $rows;
+ }
+
+ } else {
+ local $Module::Load::Conditional::VERBOSE = 0;
+ if ( can_load(modules => {'Term::Size' => '0.0'}) ) {
+ my ($cols, $rows) = Term::Size::chars();
+ return $rows;
+ }
+ }
+ return $default;
+ }
+}
+
+1;
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Shell::Default>, L<CPANPLUS::Shell::Classic>, L<cpanp>
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/Shell/Classic.pm b/lib/CPANPLUS/Shell/Classic.pm
new file mode 100644
index 0000000000..176548c4f3
--- /dev/null
+++ b/lib/CPANPLUS/Shell/Classic.pm
@@ -0,0 +1,1236 @@
+##################################################
+### CPANPLUS/Shell/Classic.pm ###
+### Backwards compatible shell for CPAN++ ###
+### Written 08-04-2002 by Jos Boumans ###
+##################################################
+
+package CPANPLUS::Shell::Classic;
+
+use strict;
+
+
+use CPANPLUS::Error;
+use CPANPLUS::Backend;
+use CPANPLUS::Configure::Setup;
+use CPANPLUS::Internals::Constants;
+
+use Cwd;
+use IPC::Cmd;
+use Term::UI;
+use Data::Dumper;
+use Term::ReadLine;
+
+use Module::Load qw[load];
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load];
+
+$Params::Check::VERBOSE = 1;
+$Params::Check::ALLOW_UNKNOWN = 1;
+
+BEGIN {
+ use vars qw[ $VERSION @ISA ];
+ @ISA = qw[ CPANPLUS::Shell::_Base::ReadLine ];
+ $VERSION = '0.0562';
+}
+
+load CPANPLUS::Shell;
+
+
+### our command set ###
+my $map = {
+ a => '_author',
+ b => '_bundle',
+ d => '_distribution',
+ 'm' => '_module',
+ i => '_find_all',
+ r => '_uptodate',
+ u => '_not_supported',
+ ls => '_ls',
+ get => '_fetch',
+ make => '_install',
+ test => '_install',
+ install => '_install',
+ clean => '_not_supported',
+ look => '_shell',
+ readme => '_readme',
+ h => '_help',
+ '?' => '_help',
+ o => '_set_conf',
+ reload => '_reload',
+ autobundle => '_autobundle',
+ '!' => '_bang',
+ #'q' => '_quit', # done it the loop itself
+};
+
+### the shell object, scoped to the file ###
+my $Shell;
+my $Brand = 'cpan';
+my $Prompt = $Brand . '> ';
+
+sub new {
+ my $class = shift;
+
+ my $cb = new CPANPLUS::Backend;
+ my $self = $class->SUPER::_init(
+ brand => $Brand,
+ term => Term::ReadLine->new( $Brand ),
+ prompt => $Prompt,
+ backend => $cb,
+ format => "%5s %-50s %8s %-10s\n",
+ );
+ ### make it available package wide ###
+ $Shell = $self;
+
+ ### enable verbose, it's the cpan.pm way
+ $cb->configure_object->set_conf( verbose => 1 );
+
+
+ ### register install callback ###
+ $cb->_register_callback(
+ name => 'install_prerequisite',
+ code => \&__ask_about_install,
+ );
+
+ ### register test report callback ###
+ $cb->_register_callback(
+ name => 'edit_test_report',
+ code => \&__ask_about_test_report,
+ );
+
+ return $self;
+}
+
+sub shell {
+ my $self = shift;
+ my $term = $self->term;
+
+ $self->_show_banner;
+ $self->_input_loop && print "\n";
+ $self->_quit;
+}
+
+sub _input_loop {
+ my $self = shift;
+ my $term = $self->term;
+ my $cb = $self->backend;
+
+ my $normal_quit = 0;
+ while (
+ defined (my $input = eval { $term->readline($self->prompt) } )
+ or $self->_signals->{INT}{count} == 1
+ ) {
+ ### re-initiate all signal handlers
+ while (my ($sig, $entry) = each %{$self->_signals} ) {
+ $SIG{$sig} = $entry->{handler} if exists($entry->{handler});
+ }
+
+ last if $self->_dispatch_on_input( input => $input );
+
+ ### flush the lib cache ###
+ $cb->_flush( list => [qw|lib load|] );
+
+ } continue {
+ $self->_signals->{INT}{count}--
+ if $self->_signals->{INT}{count}; # clear the sigint count
+ }
+
+ return 1;
+}
+
+sub _dispatch_on_input {
+ my $self = shift;
+ my $conf = $self->backend->configure_object();
+ my $term = $self->term;
+ my %hash = @_;
+
+ my $string;
+ my $tmpl = {
+ input => { required => 1, store => \$string }
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### the original force setting;
+ my $force_store = $conf->get_conf( 'force' );
+
+ ### parse the input: the first part before the space
+ ### is the command, followed by arguments.
+ ### see the usage below
+ my $key;
+ PARSE_INPUT: {
+ $string =~ s|^\s*([\w\?\!]+)\s*||;
+ chomp $string;
+ $key = lc($1);
+ }
+
+ ### you prefixed the input with 'force'
+ ### that means we set the force flag, and
+ ### reparse the input...
+ ### YAY goto block :)
+ if( $key eq 'force' ) {
+ $conf->set_conf( force => 1 );
+ goto PARSE_INPUT;
+ }
+
+ ### you want to quit
+ return 1 if $key =~ /^q/;
+
+ my $method = $map->{$key};
+ unless( $self->can( $method ) ) {
+ print "Unknown command '$key'. Type ? for help.\n";
+ return;
+ }
+
+ ### dispatch the method call
+ eval { $self->$method(
+ command => $key,
+ result => [ split /\s+/, $string ],
+ input => $string );
+ };
+ warn $@ if $@;
+
+ return;
+}
+
+### displays quit message
+sub _quit {
+
+ ### well, that's what CPAN.pm says...
+ print "Lockfile removed\n";
+}
+
+sub _not_supported {
+ my $self = shift;
+ my %hash = @_;
+
+ my $cmd;
+ my $tmpl = {
+ command => { required => 1, store => \$cmd }
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ print "Sorry, the command '$cmd' is not supported\n";
+
+ return;
+}
+
+sub _fetch {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my($aref, $input);
+ my $tmpl = {
+ result => { store => \$aref, default => [] },
+ input => { default => 'all', store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ for my $mod (@$aref) {
+ my $obj;
+
+ unless( $obj = $cb->module_tree($mod) ) {
+ print "Warning: Cannot get $input, don't know what it is\n";
+ print "Try the command\n\n";
+ print "\ti /$mod/\n\n";
+ print "to find objects with matching identifiers.\n";
+
+ next;
+ }
+
+ $obj->fetch && $obj->extract;
+ }
+
+ return $aref;
+}
+
+sub _install {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my $mapping = {
+ make => { target => TARGET_CREATE, skiptest => 1 },
+ test => { target => TARGET_CREATE },
+ install => { target => TARGET_INSTALL },
+ };
+
+ my($aref,$cmd);
+ my $tmpl = {
+ result => { store => \$aref, default => [] },
+ command => { required => 1, store => \$cmd, allow => [keys %$mapping] },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ for my $mod (@$aref) {
+ my $obj = $cb->module_tree( $mod );
+
+ unless( $obj ) {
+ print "No such module '$mod'\n";
+ next;
+ }
+
+ my $opts = $mapping->{$cmd};
+ $obj->install( %$opts );
+ }
+
+ return $aref;
+}
+
+sub _shell {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my($aref, $cmd);
+ my $tmpl = {
+ result => { store => \$aref, default => [] },
+ command => { required => 1, store => \$cmd },
+
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+
+ my $shell = $conf->get_program('shell');
+ unless( $shell ) {
+ print "Your configuration does not define a value for subshells.\n".
+ qq[Please define it with "o conf shell <your shell>"\n];
+ return;
+ }
+
+ my $cwd = Cwd::cwd();
+
+ for my $mod (@$aref) {
+ print "Running $cmd for $mod\n";
+
+ my $obj = $cb->module_tree( $mod ) or next;
+ $obj->fetch or next;
+ $obj->extract or next;
+
+ $cb->_chdir( dir => $obj->status->extract ) or next;
+
+ local $ENV{PERL5OPT} = CPANPLUS::inc->original_perl5opt;
+ if( system($shell) and $! ) {
+ print "Error executing your subshell '$shell': $!\n";
+ next;
+ }
+ }
+ $cb->_chdir( dir => $cwd );
+
+ return $aref;
+}
+
+sub _readme {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my($aref, $cmd);
+ my $tmpl = {
+ result => { store => \$aref, default => [] },
+ command => { required => 1, store => \$cmd },
+
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ for my $mod (@$aref) {
+ my $obj = $cb->module_tree( $mod ) or next;
+
+ if( my $readme = $obj->readme ) {
+
+ $self->_pager_open;
+ print $readme;
+ $self->_pager_close;
+ }
+ }
+
+ return 1;
+}
+
+sub _reload {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my($input, $cmd);
+ my $tmpl = {
+ input => { default => 'all', store => \$input },
+ command => { required => 1, store => \$cmd },
+
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ if ( $input =~ /cpan/i ) {
+ print qq[You want to reload the CPAN code\n];
+ print qq[Just type 'q' and then restart... ] .
+ qq[Trust me, it is MUCH safer\n];
+
+ } elsif ( $input =~ /index/i ) {
+ $cb->reload_indices(update_source => 1);
+
+ } else {
+ print qq[cpan re-evals the CPANPLUS.pm file\n];
+ print qq[index re-reads the index files\n];
+ }
+
+ return 1;
+}
+
+sub _autobundle {
+ my $self = shift;
+ my $cb = $self->backend;
+
+ print qq[Writing bundle file... This may take a while\n];
+
+ my $where = $cb->autobundle();
+
+ print $where
+ ? qq[\nWrote autobundle to $where\n]
+ : qq[\nCould not create autobundle\n];
+
+ return 1;
+}
+
+sub _set_conf {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my($aref, $input);
+ my $tmpl = {
+ result => { store => \$aref, default => [] },
+ input => { default => 'all', store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $type = shift @$aref;
+
+ if( $type eq 'debug' ) {
+ print qq[Sorry you cannot set debug options through ] .
+ qq[this shell in CPANPLUS\n];
+ return;
+
+ } elsif ( $type eq 'conf' ) {
+
+ ### from CPAN.pm :o)
+ # CPAN::Shell::o and CPAN::Config::edit are closely related. 'o conf'
+ # should have been called set and 'o debug' maybe 'set debug'
+
+ # commit Commit changes to disk
+ # defaults Reload defaults from disk
+ # init Interactive setting of all options
+
+ my $name = shift @$aref;
+ my $value = "@$aref";
+
+ if( $name eq 'init' ) {
+ my $setup = CPANPLUS::Configure::Setup->new(
+ conf => $cb->configure_object,
+ term => $self->term,
+ backend => $cb,
+ );
+ return $setup->init;
+
+ } elsif ($name eq 'commit' ) {;
+ $cb->configure_object->save;
+ print "Your CPAN++ configuration info has been saved!\n\n";
+ return;
+
+ } elsif ($name eq 'defaults' ) {
+ print qq[Sorry, CPANPLUS cannot restore default for you.\n] .
+ qq[Perhaps you should run the interactive setup again.\n] .
+ qq[\ttry running 'o conf init'\n];
+ return;
+
+ ### we're just supplying things in the 'conf' section now,
+ ### not the program section.. it's a bit of a hassle to make that
+ ### work cleanly with the original CPAN.pm interface, so we'll fix
+ ### it when people start complaining, which is hopefully never.
+ } else {
+ unless( $name ) {
+ my @list = grep { $_ ne 'hosts' }
+ $conf->options( type => $type );
+
+ my $method = 'get_' . $type;
+
+ local $Data::Dumper::Indent = 0;
+ for my $name ( @list ) {
+ my $val = $conf->$method($name);
+ ($val) = ref($val)
+ ? (Data::Dumper::Dumper($val) =~ /= (.*);$/)
+ : "'$val'";
+ printf " %-25s %s\n", $name, $val;
+ }
+
+ } elsif ( $name eq 'hosts' ) {
+ print "Setting hosts is not trivial.\n" .
+ "It is suggested you edit the " .
+ "configuration file manually";
+
+ } else {
+ my $method = 'set_' . $type;
+ if( $conf->$method($name => defined $value ? $value : '') ) {
+ my $set_to = defined $value ? $value : 'EMPTY STRING';
+ print "Key '$name' was set to '$set_to'\n";
+ }
+ }
+ }
+ } else {
+ print qq[Known options:\n] .
+ qq[ conf set or get configuration variables\n] .
+ qq[ debug set or get debugging options\n];
+ }
+
+ return;
+}
+
+########################
+### search functions ###
+########################
+
+sub _author {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my($aref, $short, $input, $class);
+ my $tmpl = {
+ result => { store => \$aref, default => ['/./'] },
+ short => { default => 0, store => \$short },
+ input => { default => 'all', store => \$input },
+ class => { default => 'Author', no_override => 1,
+ store => \$class },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my @regexes = map { m|/(.+)/| ? qr/$1/ : $_ } @$aref;
+
+
+ my @rv;
+ for my $type (qw[author cpanid]) {
+ push @rv, $cb->search( type => $type, allow => \@regexes );
+ }
+
+ unless( @rv ) {
+ print "No object of type $class found for argument $input\n"
+ unless $short;
+ return;
+ }
+
+ return $self->_pp_author(
+ result => \@rv,
+ class => $class,
+ short => $short,
+ input => $input );
+
+}
+
+### find all modules matching a query ###
+sub _module {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my($aref, $short, $input, $class);
+ my $tmpl = {
+ result => { store => \$aref, default => ['/./'] },
+ short => { default => 0, store => \$short },
+ input => { default => 'all', store => \$input },
+ class => { default => 'Module', no_override => 1,
+ store => \$class },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my @rv;
+ for my $module (@$aref) {
+ if( $module =~ m|/(.+)/| ) {
+ push @rv, $cb->search( type => 'module',
+ allow => [qr/$1/i] );
+ } else {
+ my $obj = $cb->module_tree( $module ) or next;
+ push @rv, $obj;
+ }
+ }
+
+ return $self->_pp_module(
+ result => \@rv,
+ class => $class,
+ short => $short,
+ input => $input );
+}
+
+### find all bundles matching a query ###
+sub _bundle {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my($aref, $short, $input, $class);
+ my $tmpl = {
+ result => { store => \$aref, default => ['/./'] },
+ short => { default => 0, store => \$short },
+ input => { default => 'all', store => \$input },
+ class => { default => 'Bundle', no_override => 1,
+ store => \$class },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my @rv;
+ for my $bundle (@$aref) {
+ if( $bundle =~ m|/(.+)/| ) {
+ push @rv, $cb->search( type => 'module',
+ allow => [qr/Bundle::.*?$1/i] );
+ } else {
+ my $obj = $cb->module_tree( "Bundle::${bundle}" ) or next;
+ push @rv, $obj;
+ }
+ }
+
+ return $self->_pp_module(
+ result => \@rv,
+ class => $class,
+ short => $short,
+ input => $input );
+}
+
+sub _distribution {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my($aref, $short, $input, $class);
+ my $tmpl = {
+ result => { store => \$aref, default => ['/./'] },
+ short => { default => 0, store => \$short },
+ input => { default => 'all', store => \$input },
+ class => { default => 'Distribution', no_override => 1,
+ store => \$class },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my @rv;
+ for my $module (@$aref) {
+ ### if it's a regex... ###
+ if ( my ($match) = $module =~ m|^/(.+)/$|) {
+
+ ### something like /FOO/Bar.tar.gz/ was entered
+ if (my ($path,$package) = $match =~ m|^/?(.+)/(.+)$|) {
+ my $seen;
+
+ my @data = $cb->search( type => 'package',
+ allow => [qr/$package/i] );
+
+ my @list = $cb->search( type => 'path',
+ allow => [qr/$path/i],
+ data => \@data );
+
+ ### make sure we dont list the same dist twice
+ for my $val ( @list ) {
+ next if $seen->{$val->package}++;
+
+ push @rv, $val;
+ }
+
+ ### something like /FOO/ or /Bar.tgz/ was entered
+ ### so we look both in the path, as well as in the package name
+ } else {
+ my $seen;
+ { my @list = $cb->search( type => 'package',
+ allow => [qr/$match/i] );
+
+ ### make sure we dont list the same dist twice
+ for my $val ( @list ) {
+ next if $seen->{$val->package}++;
+
+ push @rv, $val;
+ }
+ }
+
+ { my @list = $cb->search( type => 'path',
+ allow => [qr/$match/i] );
+
+ ### make sure we dont list the same dist twice
+ for my $val ( @list ) {
+ next if $seen->{$val->package}++;
+
+ push @rv, $val;
+ }
+
+ }
+ }
+
+ } else {
+
+ ### user entered a full dist, like: R/RC/RCAPUTO/POE-0.19.tar.gz
+ if (my ($path,$package) = $module =~ m|^/?(.+)/(.+)$|) {
+ my @data = $cb->search( type => 'package',
+ allow => [qr/^$package$/] );
+ my @list = $cb->search( type => 'path',
+ allow => [qr/$path$/i],
+ data => \@data);
+
+ ### make sure we dont list the same dist twice
+ my $seen;
+ for my $val ( @list ) {
+ next if $seen->{$val->package}++;
+
+ push @rv, $val;
+ }
+ }
+ }
+ }
+
+ return $self->_pp_distribution(
+ result => \@rv,
+ class => $class,
+ short => $short,
+ input => $input );
+}
+
+sub _find_all {
+ my $self = shift;
+
+ my @rv;
+ for my $method (qw[_author _bundle _module _distribution]) {
+ my $aref = $self->$method( @_, short => 1 );
+
+ push @rv, @$aref if $aref;
+ }
+
+ print scalar(@rv). " items found\n"
+}
+
+sub _uptodate {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my($aref, $short, $input, $class);
+ my $tmpl = {
+ result => { store => \$aref, default => ['/./'] },
+ short => { default => 0, store => \$short },
+ input => { default => 'all', store => \$input },
+ class => { default => 'Uptodate', no_override => 1,
+ store => \$class },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+
+ my @rv;
+ if( @$aref) {
+ for my $module (@$aref) {
+ if( $module =~ m|/(.+)/| ) {
+ my @list = $cb->search( type => 'module',
+ allow => [qr/$1/i] );
+
+ ### only add those that are installed and not core
+ push @rv, grep { not $_->package_is_perl_core }
+ grep { $_->installed_file }
+ @list;
+
+ } else {
+ my $obj = $cb->module_tree( $module ) or next;
+ push @rv, $obj;
+ }
+ }
+ } else {
+ @rv = @{$cb->_all_installed};
+ }
+
+ return $self->_pp_uptodate(
+ result => \@rv,
+ class => $class,
+ short => $short,
+ input => $input );
+}
+
+sub _ls {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my($aref, $short, $input, $class);
+ my $tmpl = {
+ result => { store => \$aref, default => [] },
+ short => { default => 0, store => \$short },
+ input => { default => 'all', store => \$input },
+ class => { default => 'Uptodate', no_override => 1,
+ store => \$class },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my @rv;
+ for my $name (@$aref) {
+ my $auth = $cb->author_tree( uc $name );
+
+ unless( $auth ) {
+ print qq[ls command rejects argument $name: not an author\n];
+ next;
+ }
+
+ push @rv, $auth->distributions;
+ }
+
+ return $self->_pp_ls(
+ result => \@rv,
+ class => $class,
+ short => $short,
+ input => $input );
+}
+
+############################
+### pretty printing subs ###
+############################
+
+
+sub _pp_author {
+ my $self = shift;
+ my %hash = @_;
+
+ my( $aref, $short, $class, $input );
+ my $tmpl = {
+ result => { required => 1, default => [], strict_type => 1,
+ store => \$aref },
+ short => { default => 0, store => \$short },
+ class => { required => 1, store => \$class },
+ input => { required => 1, store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### no results
+ if( !@$aref ) {
+ print "No objects of type $class found for argument $input\n"
+ unless $short;
+
+ ### one result, long output desired;
+ } elsif( @$aref == 1 and !$short ) {
+
+ ### should look like this:
+ #cpan> a KANE
+ #Author id = KANE
+ # EMAIL boumans@frg.eur.nl
+ # FULLNAME Jos Boumans
+
+ my $obj = shift @$aref;
+
+ print "$class id = ", $obj->cpanid(), "\n";
+ printf " %-12s %s\n", 'EMAIL', $obj->email();
+ printf " %-12s %s%s\n", 'FULLNAME', $obj->author();
+
+ } else {
+
+ ### should look like this:
+ #Author KANE (Jos Boumans)
+ #Author LBROCARD (Leon Brocard)
+ #2 items found
+
+ for my $obj ( @$aref ) {
+ printf qq[%-15s %s ("%s" (%s))\n],
+ $class, $obj->cpanid, $obj->author, $obj->email;
+ }
+ print scalar(@$aref)." items found\n" unless $short;
+ }
+
+ return $aref;
+}
+
+sub _pp_module {
+ my $self = shift;
+ my %hash = @_;
+
+ my( $aref, $short, $class, $input );
+ my $tmpl = {
+ result => { required => 1, default => [], strict_type => 1,
+ store => \$aref },
+ short => { default => 0, store => \$short },
+ class => { required => 1, store => \$class },
+ input => { required => 1, store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+
+ ### no results
+ if( !@$aref ) {
+ print "No objects of type $class found for argument $input\n"
+ unless $short;
+
+ ### one result, long output desired;
+ } elsif( @$aref == 1 and !$short ) {
+
+
+ ### should look like this:
+ #Module id = LWP
+ # DESCRIPTION Libwww-perl
+ # CPAN_USERID GAAS (Gisle Aas <gisle@ActiveState.com>)
+ # CPAN_VERSION 5.64
+ # CPAN_FILE G/GA/GAAS/libwww-perl-5.64.tar.gz
+ # DSLI_STATUS RmpO (released,mailing-list,perl,object-oriented)
+ # MANPAGE LWP - The World-Wide Web library for Perl
+ # INST_FILE C:\Perl\site\lib\LWP.pm
+ # INST_VERSION 5.62
+
+ my $obj = shift @$aref;
+ my $aut_obj = $obj->author;
+ my $format = " %-12s %s%s\n";
+
+ print "$class id = ", $obj->module(), "\n";
+ printf $format, 'DESCRIPTION', $obj->description()
+ if $obj->description();
+
+ printf $format, 'CPAN_USERID', $aut_obj->cpanid() . " (" .
+ $aut_obj->author() . " <" . $aut_obj->email() . ">)";
+
+ printf $format, 'CPAN_VERSION', $obj->version();
+ printf $format, 'CPAN_FILE', $obj->path() . '/' . $obj->package();
+
+ printf $format, 'DSLI_STATUS', $self->_pp_dslip($obj->dslip)
+ if $obj->dslip() =~ /\w/;
+
+ #printf $format, 'MANPAGE', $obj->foo();
+ ### this is for bundles... CPAN.pm downloads them,
+ #printf $format, 'CONATAINS,
+ # parses and goes from there...
+
+ printf $format, 'INST_FILE', $obj->installed_file ||
+ '(not installed)';
+ printf $format, 'INST_VERSION', $obj->installed_version;
+
+
+
+ } else {
+
+ ### should look like this:
+ #Module LWP (G/GA/GAAS/libwww-perl-5.64.tar.gz)
+ #Module POE (R/RC/RCAPUTO/POE-0.19.tar.gz)
+ #2 items found
+
+ for my $obj ( @$aref ) {
+ printf "%-15s %-15s (%s)\n", $class, $obj->module(),
+ $obj->path() .'/'. $obj->package();
+ }
+ print scalar(@$aref). " items found\n" unless $short;
+ }
+
+ return $aref;
+}
+
+sub _pp_dslip {
+ my $self = shift;
+ my $dslip = shift or return;
+
+ my (%_statusD, %_statusS, %_statusL, %_statusI);
+
+ @_statusD{qw(? i c a b R M S)} =
+ qw(unknown idea pre-alpha alpha beta released mature standard);
+
+ @_statusS{qw(? m d u n)} =
+ qw(unknown mailing-list developer comp.lang.perl.* none);
+
+ @_statusL{qw(? p c + o h)} = qw(unknown perl C C++ other hybrid);
+ @_statusI{qw(? f r O h)} =
+ qw(unknown functions references+ties object-oriented hybrid);
+
+ my @status = split("", $dslip);
+
+ my $results = sprintf( "%s (%s,%s,%s,%s)",
+ $dslip,
+ $_statusD{$status[0]},
+ $_statusS{$status[1]},
+ $_statusL{$status[2]},
+ $_statusI{$status[3]}
+ );
+
+ return $results;
+}
+
+sub _pp_distribution {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my( $aref, $short, $class, $input );
+ my $tmpl = {
+ result => { required => 1, default => [], strict_type => 1,
+ store => \$aref },
+ short => { default => 0, store => \$short },
+ class => { required => 1, store => \$class },
+ input => { required => 1, store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+
+ ### no results
+ if( !@$aref ) {
+ print "No objects of type $class found for argument $input\n"
+ unless $short;
+
+ ### one result, long output desired;
+ } elsif( @$aref == 1 and !$short ) {
+
+
+ ### should look like this:
+ #Distribution id = S/SA/SABECK/POE-Component-Client-POP3-0.02.tar.gz
+ # CPAN_USERID SABECK (Scott Beck <scott@gossamer-threads.com>)
+ # CONTAINSMODS POE::Component::Client::POP3
+
+ my $obj = shift @$aref;
+ my $aut_obj = $obj->author;
+ my $pkg = $obj->package;
+ my $format = " %-12s %s\n";
+
+ my @list = $cb->search( type => 'package',
+ allow => [qr/^$pkg$/] );
+
+
+ print "$class id = ", $obj->path(), '/', $obj->package(), "\n";
+ printf $format, 'CPAN_USERID',
+ $aut_obj->cpanid .' ('. $aut_obj->author .
+ ' '. $aut_obj->email .')';
+
+ ### yes i know it's ugly, but it's what cpan.pm does
+ printf $format, 'CONTAINSMODS', join (' ', map { $_->module } @list);
+
+ } else {
+
+ ### should look like this:
+ #Distribution LWP (G/GA/GAAS/libwww-perl-5.64.tar.gz)
+ #Distribution POE (R/RC/RCAPUTO/POE-0.19.tar.gz)
+ #2 items found
+
+ for my $obj ( @$aref ) {
+ printf "%-15s %s\n", $class, $obj->path() .'/'. $obj->package();
+ }
+
+ print scalar(@$aref). " items found\n" unless $short;
+ }
+
+ return $aref;
+}
+
+sub _pp_uptodate {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my( $aref, $short, $class, $input );
+ my $tmpl = {
+ result => { required => 1, default => [], strict_type => 1,
+ store => \$aref },
+ short => { default => 0, store => \$short },
+ class => { required => 1, store => \$class },
+ input => { required => 1, store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ my $format = "%-25s %9s %9s %s\n";
+
+ my @not_uptodate;
+ my $no_version;
+
+ my %seen;
+ for my $mod (@$aref) {
+ next if $mod->package_is_perl_core;
+ next if $seen{ $mod->package }++;
+
+
+ if( $mod->installed_file and not $mod->installed_version ) {
+ $no_version++;
+ next;
+ }
+
+ push @not_uptodate, $mod unless $mod->is_uptodate;
+ }
+
+ unless( @not_uptodate ) {
+ my $string = $input
+ ? "for $input"
+ : '';
+ print "All modules are up to date $string\n";
+ return;
+
+ } else {
+ printf $format, ( 'Package namespace',
+ 'installed',
+ 'latest',
+ 'in CPAN file'
+ );
+ }
+
+ for my $mod ( sort { $a->module cmp $b->module } @not_uptodate ) {
+ printf $format, ( $mod->module,
+ $mod->installed_version,
+ $mod->version,
+ $mod->path .'/'. $mod->package,
+ );
+ }
+
+ print "$no_version installed modules have no (parsable) version number\n"
+ if $no_version;
+
+ return \@not_uptodate;
+}
+
+sub _pp_ls {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my( $aref, $short, $class, $input );
+ my $tmpl = {
+ result => { required => 1, default => [], strict_type => 1,
+ store => \$aref },
+ short => { default => 0, store => \$short },
+ class => { required => 1, store => \$class },
+ input => { required => 1, store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### should look something like this:
+ #6272 2002-05-12 KANE/Acme-Comment-1.00.tar.gz
+ #8171 2002-08-13 KANE/Acme-Comment-1.01.zip
+ #7110 2002-09-04 KANE/Acme-Comment-1.02.tar.gz
+ #7571 2002-09-08 KANE/Acme-Intraweb-1.01.tar.gz
+ #6625 2001-08-23 KANE/Acme-POE-Knee-1.10.zip
+ #3058 2003-10-05 KANE/Acme-Test-0.02.tar.gz
+
+ ### don't know size or mtime
+ #my $format = "%8d %10s %s/%s\n";
+
+ for my $mod ( sort { $a->package cmp $b->package } @$aref ) {
+ print "\t" . $mod->package . "\n";
+ }
+
+ return $aref;
+}
+
+
+#############################
+### end pretty print subs ###
+#############################
+
+
+sub _bang {
+ my $self = shift;
+ my %hash = @_;
+
+ my( $input );
+ my $tmpl = {
+ input => { required => 1, store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ eval $input;
+ warn $@ if $@;
+
+ print "\n";
+
+ return;
+}
+
+sub _help {
+ print qq[
+Display Information
+ a authors
+ b string display bundles
+ d or info distributions
+ m /regex/ about modules
+ i or anything of above
+ r none reinstall recommendations
+ u uninstalled distributions
+
+Download, Test, Make, Install...
+ get download
+ make make (implies get)
+ test modules, make test (implies make)
+ install dists, bundles make install (implies test)
+ clean make clean
+ look open subshell in these dists' directories
+ readme display these dists' README files
+
+Other
+ h,? display this menu ! perl-code eval a perl command
+ o conf [opt] set and query options q quit the cpan shell
+ reload cpan load CPAN.pm again reload index load newer indices
+ autobundle Snapshot force cmd unconditionally do cmd
+];
+
+}
+
+
+
+1;
+__END__
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Shell::Classic - CPAN.pm emulation for CPANPLUS
+
+=head1 DESCRIPTION
+
+The Classic shell is designed to provide the feel of the CPAN.pm shell
+using CPANPLUS underneath.
+
+For detailed documentation, refer to L<CPAN>.
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Configure>, L<CPANPLUS::Module>, L<CPANPLUS::Module::Author>
+
+=cut
+
+
+=head1 SEE ALSO
+
+L<CPAN>
+
+=cut
+
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/Shell/Default.pm b/lib/CPANPLUS/Shell/Default.pm
new file mode 100644
index 0000000000..c65cb88d37
--- /dev/null
+++ b/lib/CPANPLUS/Shell/Default.pm
@@ -0,0 +1,1699 @@
+package CPANPLUS::Shell::Default;
+
+use strict;
+
+
+use CPANPLUS::Error;
+use CPANPLUS::Backend;
+use CPANPLUS::Configure::Setup;
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Internals::Constants::Report qw[GRADE_FAIL];
+
+use Cwd;
+use IPC::Cmd;
+use Term::UI;
+use Data::Dumper;
+use Term::ReadLine;
+
+use Module::Load qw[load];
+use Params::Check qw[check];
+use Module::Load::Conditional qw[can_load check_install];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+local $Params::Check::VERBOSE = 1;
+local $Data::Dumper::Indent = 1; # for dumpering from !
+
+BEGIN {
+ use vars qw[ $VERSION @ISA ];
+ @ISA = qw[ CPANPLUS::Shell::_Base::ReadLine ];
+ $VERSION = "0.78";
+}
+
+load CPANPLUS::Shell;
+
+
+my $map = {
+ 'm' => '_search_module',
+ 'a' => '_search_author',
+ '!' => '_bang',
+ '?' => '_help',
+ 'h' => '_help',
+ 'q' => '_quit',
+ 'r' => '_readme',
+ 'v' => '_show_banner',
+ 'w' => '__display_results',
+ 'd' => '_fetch',
+ 'z' => '_shell',
+ 'f' => '_distributions',
+ 'x' => '_reload_indices',
+ 'i' => '_install',
+ 't' => '_install',
+ 'l' => '_details',
+ 'p' => '_print',
+ 's' => '_set_conf',
+ 'o' => '_uptodate',
+ 'b' => '_autobundle',
+ 'u' => '_uninstall',
+ '/' => '_meta', # undocumented for now
+ 'c' => '_reports',
+};
+### free letters: e g j k n y ###
+
+
+### will be filled if you have a .default-shell.rc and
+### Config::Auto installed
+my $rc = {};
+
+### the shell object, scoped to the file ###
+my $Shell;
+my $Brand = loc('CPAN Terminal');
+my $Prompt = $Brand . '> ';
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::Shell::Default
+
+=head1 SYNOPSIS
+
+ ### loading the shell:
+ $ cpanp # run 'cpanp' from the command line
+ $ perl -MCPANPLUS -eshell # load the shell from the command line
+
+
+ use CPANPLUS::Shell qw[Default]; # load this shell via the API
+ # always done via CPANPLUS::Shell
+
+ my $ui = CPANPLUS::Shell->new;
+ $ui->shell; # run the shell
+ $ui->dispatch_on_input( input => 'x'); # update the source using the
+ # dispatch method
+
+ ### when in the shell:
+ ### Note that all commands can also take options.
+ ### Look at their underlying CPANPLUS::Backend methods to see
+ ### what options those are.
+ cpanp> h # show help messages
+ cpanp> ? # show help messages
+
+ cpanp> m Acme # find acme modules, allows regexes
+ cpanp> a KANE # find modules by kane, allows regexes
+ cpanp> f Acme::Foo # get a list of all releases of Acme::Foo
+
+ cpanp> i Acme::Foo # install Acme::Foo
+ cpanp> i Acme-Foo-1.3 # install version 1.3 of Acme::Foo
+ cpanp> i <URI> # install from URI, like ftp://foo.com/X.tgz
+ cpanp> i 1 3..5 # install search results 1, 3, 4 and 5
+ cpanp> i * # install all search results
+ cpanp> a KANE; i *; # find modules by kane, install all results
+ cpanp> t Acme::Foo # test Acme::Foo, without installing it
+ cpanp> u Acme::Foo # uninstall Acme::Foo
+ cpanp> d Acme::Foo # download Acme::Foo
+ cpanp> z Acme::Foo # download & extract Acme::Foo, then open a
+ # shell in the extraction directory
+
+ cpanp> c Acme::Foo # get a list of test results for Acme::Foo
+ cpanp> l Acme::Foo # view details about the Acme::Foo package
+ cpanp> r Acme::Foo # view Acme::Foo's README file
+ cpanp> o # get a list of all installed modules that
+ # are out of date
+ cpanp> o 1..3 # list uptodateness from a previous search
+
+ cpanp> s conf # show config settings
+ cpanp> s conf md5 1 # enable md5 checks
+ cpanp> s program # show program settings
+ cpanp> s edit # edit config file
+ cpanp> s reconfigure # go through initial configuration again
+ cpanp> s selfupdate # update your CPANPLUS install
+ cpanp> s save # save config to disk
+ cpanp> s mirrors # show currently selected mirrors
+
+ cpanp> ! [PERL CODE] # execute the following perl code
+
+ cpanp> b # create an autobundle for this computers
+ # perl installation
+ cpanp> x # reload index files (purges cache)
+ cpanp> x --update_source # reload index files, get fresh source files
+ cpanp> p [FILE] # print error stack (to a file)
+ cpanp> v # show the banner
+ cpanp> w # show last search results again
+
+ cpanp> q # quit the shell
+
+ cpanp> /plugins # list avialable plugins
+ cpanp> /? PLUGIN # list help test of <PLUGIN>
+
+ ### common options:
+ cpanp> i ... --skiptest # skip tests
+ cpanp> i ... --force # force all operations
+ cpanp> i ... --verbose # run in verbose mode
+
+=head1 DESCRIPTION
+
+This module provides the default user interface to C<CPANPLUS>. You
+can start it via the C<cpanp> binary, or as detailed in the L<SYNOPSIS>.
+
+=cut
+
+sub new {
+ my $class = shift;
+
+ my $cb = new CPANPLUS::Backend;
+ my $self = $class->SUPER::_init(
+ brand => $Brand,
+ term => Term::ReadLine->new( $Brand ),
+ prompt => $Prompt,
+ backend => $cb,
+ format => "%4s %-55s %8s %-10s\n",
+ dist_format => "%4s %-42s %-12s %8s %-10s\n",
+ );
+ ### make it available package wide ###
+ $Shell = $self;
+
+ my $rc_file = File::Spec->catfile(
+ $cb->configure_object->get_conf('base'),
+ DOT_SHELL_DEFAULT_RC,
+ );
+
+
+ if( -e $rc_file && -r _ ) {
+ $rc = _read_configuration_from_rc( $rc_file );
+ }
+
+ ### register install callback ###
+ $cb->_register_callback(
+ name => 'install_prerequisite',
+ code => \&__ask_about_install,
+ );
+
+ ### execute any login commands specified ###
+ $self->dispatch_on_input( input => $rc->{'login'} )
+ if defined $rc->{'login'};
+
+ ### register test report callbacks ###
+ $cb->_register_callback(
+ name => 'edit_test_report',
+ code => \&__ask_about_edit_test_report,
+ );
+
+ $cb->_register_callback(
+ name => 'send_test_report',
+ code => \&__ask_about_send_test_report,
+ );
+
+
+ return $self;
+}
+
+sub shell {
+ my $self = shift;
+ my $term = $self->term;
+ my $conf = $self->backend->configure_object;
+
+ $self->_show_banner;
+ print "*** Type 'p' now to show start up log\n"; # XXX add to banner?
+ $self->_show_random_tip if $conf->get_conf('show_startup_tip');
+ $self->_input_loop && print "\n";
+ $self->_quit;
+}
+
+sub _input_loop {
+ my $self = shift;
+ my $term = $self->term;
+ my $cb = $self->backend;
+
+ my $normal_quit = 0;
+ while (
+ defined (my $input = eval { $term->readline($self->prompt) } )
+ or $self->_signals->{INT}{count} == 1
+ ) {
+ ### re-initiate all signal handlers
+ while (my ($sig, $entry) = each %{$self->_signals} ) {
+ $SIG{$sig} = $entry->{handler} if exists($entry->{handler});
+ }
+
+ print "\n";
+ last if $self->dispatch_on_input( input => $input );
+
+ ### flush the lib cache ###
+ $cb->_flush( list => [qw|lib load|] );
+
+ } continue {
+ $self->_signals->{INT}{count}--
+ if $self->_signals->{INT}{count}; # clear the sigint count
+ }
+
+ return 1;
+}
+
+### return 1 to quit ###
+sub dispatch_on_input {
+ my $self = shift;
+ my $conf = $self->backend->configure_object();
+ my $term = $self->term;
+ my %hash = @_;
+
+ my($string, $noninteractive);
+ my $tmpl = {
+ input => { required => 1, store => \$string },
+ noninteractive => { required => 0, store => \$noninteractive },
+ };
+
+ check( $tmpl, \%hash ) or return;
+
+ ### indicates whether or not the user will receive a shell
+ ### prompt after the command has finished.
+ $self->noninteractive($noninteractive) if defined $noninteractive;
+
+ my @cmds = split ';', $string;
+ while( my $input = shift @cmds ) {
+
+ ### to send over the socket ###
+ my $org_input = $input;
+
+ my $key; my $options;
+ { ### make whitespace not count when using special chars
+ { $input =~ s|^\s*([!?/])|$1 |; }
+
+ ### get the first letter of the input
+ $input =~ s|^\s*([\w\?\!/])\w*||;
+
+ chomp $input;
+ $key = lc($1);
+
+ ### we figured out what the command was...
+ ### if we have more input, that DOES NOT start with a white
+ ### space char, we misparsed.. like 'Test::Foo::Bar', which
+ ### would turn into 't', '::Foo::Bar'...
+ if( $input and $input !~ s/^\s+// ) {
+ print loc("Could not understand command: %1\n".
+ "Possibly missing command before argument(s)?\n",
+ $org_input);
+ return;
+ }
+
+ ### allow overrides from the config file ###
+ if( defined $rc->{$key} ) {
+ $input = $rc->{$key} . $input;
+ }
+
+ ### grab command line options like --no-force and --verbose ###
+ ($options,$input) = $term->parse_options($input)
+ unless $key eq '!';
+ }
+
+ ### emtpy line? ###
+ return unless $key;
+
+ ### time to quit ###
+ return 1 if $key eq 'q';
+
+ my $method = $map->{$key};
+
+ ### dispatch meta locally at all times ###
+ $self->$method(input => $input, options => $options), next
+ if $key eq '/';
+
+ ### flush unless we're trying to print the stack
+ CPANPLUS::Error->flush unless $key eq 'p';
+
+ ### connected over a socket? ###
+ if( $self->remote ) {
+
+ ### unsupported commands ###
+ if( $key eq 'z' or
+ ($key eq 's' and $input =~ /^\s*edit/)
+ ) {
+ print "\n", loc("Command not supported over remote connection"),
+ "\n\n";
+
+ } else {
+ my($status,$buff) = $self->__send_remote_command($org_input);
+
+ print "\n", loc("Command failed!"), "\n\n" unless $status;
+
+ $self->_pager_open if $buff =~ tr/\n// > $self->_term_rowcount;
+ print $buff;
+ $self->_pager_close;
+ }
+
+ ### or just a plain local shell? ###
+ } else {
+
+ unless( $self->can($method) ) {
+ print loc("Unknown command '%1'. Usage:", $key), "\n";
+ $self->_help;
+
+ } else {
+
+ ### some methods don't need modules ###
+ my @mods;
+ @mods = $self->_select_modules($input)
+ unless grep {$key eq $_} qw[! m a v w x p s b / ? h];
+
+ eval { $self->$method( modules => \@mods,
+ options => $options,
+ input => $input,
+ choice => $key )
+ };
+ error( $@ ) if $@;
+ }
+ }
+ }
+
+ return;
+}
+
+sub _select_modules {
+ my $self = shift;
+ my $input = shift or return;
+ my $cache = $self->cache;
+ my $cb = $self->backend;
+
+ ### expand .. in $input
+ $input =~ s{\b(\d+)\s*\.\.\s*(\d+)\b}
+ {join(' ', ($1 < 1 ? 1 : $1) .. ($2 > $#{$cache} ? $#{$cache} : $2))}eg;
+
+ $input = join(' ', 1 .. $#{$cache}) if $input eq '*';
+ $input =~ s/'/::/g; # perl 4 convention
+
+ my @rv;
+ for my $mod (split /\s+/, $input) {
+
+ ### it's a cache look up ###
+ if( $mod =~ /^\d+/ and $mod > 0 ) {
+ unless( scalar @$cache ) {
+ print loc("No search was done yet!"), "\n";
+
+ } elsif ( my $obj = $cache->[$mod] ) {
+ push @rv, $obj;
+
+ } else {
+ print loc("No such module: %1", $mod), "\n";
+ }
+
+ } else {
+ my $obj = $cb->parse_module( module => $mod );
+
+ unless( $obj ) {
+ print loc("No such module: %1", $mod), "\n";
+
+ } else {
+ push @rv, $obj;
+ }
+ }
+ }
+
+ unless( scalar @rv ) {
+ print loc("No modules found to operate on!\n");
+ return;
+ } else {
+ return @rv;
+ }
+}
+
+sub _format_version {
+ my $self = shift;
+ my $version = shift;
+
+ ### fudge $version into the 'optimal' format
+ $version = 0 if $version eq 'undef';
+ $version =~ s/_//g; # everything after gets stripped off otherwise
+
+ ### allow 6 digits after the dot, as that's how perl stringifies
+ ### x.y.z numbers.
+ $version = sprintf('%3.6f', $version);
+ $version = '' if $version == '0.00';
+ $version =~ s/(00{0,3})$/' ' x (length $1)/e;
+
+ return $version;
+}
+
+sub __display_results {
+ my $self = shift;
+ my $cache = $self->cache;
+
+ my @rv = @$cache;
+
+ if( scalar @rv ) {
+
+ $self->_pager_open if $#{$cache} >= $self->_term_rowcount;
+
+ my $i = 1;
+ for my $mod (@rv) {
+ next unless $mod; # first one is undef
+ # humans start counting at 1
+
+ ### for dists only -- we have checksum info
+ if( $mod->mtime ) {
+ printf $self->dist_format,
+ $i,
+ $mod->module,
+ $mod->mtime,
+ $self->_format_version($mod->version),
+ $mod->author->cpanid();
+
+ } else {
+ printf $self->format,
+ $i,
+ $mod->module,
+ $self->_format_version($mod->version),
+ $mod->author->cpanid();
+ }
+ $i++;
+ }
+
+ $self->_pager_close;
+
+ } else {
+ print loc("No results to display"), "\n";
+ }
+}
+
+
+sub _quit {
+ my $self = shift;
+
+ $self->dispatch_on_input( input => $rc->{'logout'} )
+ if defined $rc->{'logout'};
+
+ print loc("Exiting CPANPLUS shell"), "\n";
+}
+
+###########################
+### actual command subs ###
+###########################
+
+
+### print out the help message ###
+### perhaps, '?' should be a slightly different version ###
+my @Help;
+sub _help {
+ my $self = shift;
+ my %hash = @_;
+
+ my $input;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ input => { required => 0, store => \$input }
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+ }
+
+ @Help = (
+loc('[General]' ),
+loc(' h | ? # display help' ),
+loc(' q # exit' ),
+loc(' v # version information' ),
+loc('[Search]' ),
+loc(' a AUTHOR ... # search by author(s)' ),
+loc(' m MODULE ... # search by module(s)' ),
+loc(' f MODULE ... # list all releases of a module' ),
+loc(" o [ MODULE ... ] # list installed module(s) that aren't up to date" ),
+loc(' w # display the result of your last search again' ),
+loc('[Operations]' ),
+loc(' i MODULE | NUMBER ... # install module(s), by name or by search number' ),
+loc(' i URI | ... # install module(s), by URI (ie http://foo.com/X.tgz)' ),
+loc(' t MODULE | NUMBER ... # test module(s), by name or by search number' ),
+loc(' u MODULE | NUMBER ... # uninstall module(s), by name or by search number' ),
+loc(' d MODULE | NUMBER ... # download module(s)' ),
+loc(' l MODULE | NUMBER ... # display detailed information about module(s)' ),
+loc(' r MODULE | NUMBER ... # display README files of module(s)' ),
+loc(' c MODULE | NUMBER ... # check for module report(s) from cpan-testers' ),
+loc(' z MODULE | NUMBER ... # extract module(s) and open command prompt in it' ),
+loc('[Local Administration]' ),
+loc(' b # write a bundle file for your configuration' ),
+loc(' s program [OPT VALUE] # set program locations for this session' ),
+loc(' s conf [OPT VALUE] # set config options for this session' ),
+loc(' s mirrors # show currently selected mirrors' ),
+loc(' s reconfigure # reconfigure settings ' ),
+loc(' s selfupdate # update your CPANPLUS install '),
+loc(' s save [user|system] # save settings for this user or systemwide' ),
+loc(' s edit [user|system] # open configuration file in editor and reload' ),
+loc(' ! EXPR # evaluate a perl statement' ),
+loc(' p [FILE] # print the error stack (optionally to a file)' ),
+loc(' x # reload CPAN indices (purges cache)' ),
+loc(' x --update_source # reload CPAN indices, get fresh source files' ),
+loc('[Plugins]' ),
+loc(' /plugins # list available plugins' ),
+loc(' /? [PLUGIN NAME] # show usage for (a particular) plugin(s)' ),
+
+ ) unless @Help;
+
+ $self->_pager_open if (@Help >= $self->_term_rowcount);
+ ### XXX: functional placeholder for actual 'detailed' help.
+ print "Detailed help for the command '$input' is not available.\n\n"
+ if length $input;
+ print map {"$_\n"} @Help;
+ print $/;
+ $self->_pager_close;
+}
+
+### eval some code ###
+sub _bang {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+
+ my $input;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ input => { required => 1, store => \$input }
+ };
+
+ my $args = check( $tmpl, \%hash ) or return;
+ }
+
+ local $Data::Dumper::Indent = 1; # for dumpering from !
+ eval $input;
+ error( $@ ) if $@;
+ print "\n";
+ return;
+}
+
+sub _search_module {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my $args;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ input => { required => 1, },
+ options => { default => { } },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ my @regexes = map { qr/$_/i } split /\s+/, $args->{'input'};
+
+ ### XXX this is rather slow, because (probably)
+ ### of the many method calls
+ ### XXX need to profile to speed it up =/
+
+ ### find the modules ###
+ my @rv = sort { $a->module cmp $b->module }
+ $cb->search(
+ %{$args->{'options'}},
+ type => 'module',
+ allow => \@regexes,
+ );
+
+ ### store the result in the cache ###
+ $self->cache([undef,@rv]);
+
+ $self->__display_results;
+
+ return 1;
+}
+
+sub _search_author {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my $args;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ input => { required => 1, },
+ options => { default => { } },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ my @regexes = map { qr/$_/i } split /\s+/, $args->{'input'};
+
+ my @rv;
+ for my $type (qw[author cpanid]) {
+ push @rv, $cb->search(
+ %{$args->{'options'}},
+ type => $type,
+ allow => \@regexes,
+ );
+ }
+
+ my %seen;
+ my @list = sort { $a->module cmp $b->module }
+ grep { defined }
+ map { $_->modules }
+ grep { not $seen{$_}++ } @rv;
+
+ $self->cache([undef,@list]);
+
+ $self->__display_results;
+ return 1;
+}
+
+sub _readme {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my $args; my $mods; my $opts;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ modules => { required => 1, store => \$mods },
+ options => { default => { }, store => \$opts },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ return unless scalar @$mods;
+
+ $self->_pager_open;
+ for my $mod ( @$mods ) {
+ print $mod->readme( %$opts );
+ }
+
+ $self->_pager_close;
+
+ return 1;
+}
+
+sub _fetch {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my $args; my $mods; my $opts;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ modules => { required => 1, store => \$mods },
+ options => { default => { }, store => \$opts },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ $self->_pager_open if @$mods >= $self->_term_rowcount;
+ for my $mod (@$mods) {
+ my $where = $mod->fetch( %$opts );
+
+ print $where
+ ? loc("Successfully fetched '%1' to '%2'",
+ $mod->module, $where )
+ : loc("Failed to fetch '%1'", $mod->module);
+ print "\n";
+ }
+ $self->_pager_close;
+
+}
+
+sub _shell {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $shell = $conf->get_program('shell');
+ unless( $shell ) {
+ print loc("Your config does not specify a subshell!"), "\n",
+ loc("Perhaps you need to re-run your setup?"), "\n";
+ return;
+ }
+
+ my $args; my $mods; my $opts;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ modules => { required => 1, store => \$mods },
+ options => { default => { }, store => \$opts },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ my $cwd = Cwd::cwd();
+ for my $mod (@$mods) {
+ $mod->fetch( %$opts ) or next;
+ $mod->extract( %$opts ) or next;
+
+ $cb->_chdir( dir => $mod->status->extract() ) or next;
+
+ #local $ENV{PERL5OPT} = CPANPLUS::inc->original_perl5opt;
+
+ if( system($shell) and $! ) {
+ print loc("Error executing your subshell '%1': %2",
+ $shell, $!),"\n";
+ next;
+ }
+ }
+ $cb->_chdir( dir => $cwd );
+
+ return 1;
+}
+
+sub _distributions {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $args; my $mods; my $opts;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ modules => { required => 1, store => \$mods },
+ options => { default => { }, store => \$opts },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ my @list;
+ for my $mod (@$mods) {
+ push @list, sort { $a->version <=> $b->version }
+ grep { defined } $mod->distributions( %$opts );
+ }
+
+ my @rv = sort { $a->module cmp $b->module } @list;
+
+ $self->cache([undef,@rv]);
+ $self->__display_results;
+
+ return; 1;
+}
+
+sub _reload_indices {
+ my $self = shift;
+ my $cb = $self->backend;
+ my %hash = @_;
+
+ my $args; my $opts;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ my $rv = $cb->reload_indices( %$opts );
+
+ ### so the update failed, but you didnt give it any options either
+ if( !$rv and !(keys %$opts) ) {
+ print "\nFailure may be due to corrupt source files\n" .
+ "Try this:\n\tx --update_source\n\n";
+ }
+
+ return $rv;
+
+}
+
+sub _install {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $args; my $mods; my $opts; my $choice;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ modules => { required => 1, store => \$mods },
+ options => { default => { }, store => \$opts },
+ choice => { required => 1, store => \$choice,
+ allow => [qw|i t|] },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ unless( scalar @$mods ) {
+ print loc("Nothing done\n");
+ return;
+ }
+
+ my $target = $choice eq 'i' ? TARGET_INSTALL : TARGET_CREATE;
+ my $prompt = $choice eq 'i' ? loc('Installing ') : loc('Testing ');
+ my $action = $choice eq 'i' ? 'install' : 'test';
+
+ my $status = {};
+ ### first loop over the mods to install them ###
+ for my $mod (@$mods) {
+ print $prompt, $mod->module, "\n";
+
+ my $log_length = length CPANPLUS::Error->stack_as_string;
+
+ ### store the status for look up when we're done with all
+ ### install calls
+ $status->{$mod} = $mod->install( %$opts, target => $target );
+
+ ### would you like a log file of what happened?
+ if( $conf->get_conf('write_install_logs') ) {
+
+ my $dir = File::Spec->catdir(
+ $conf->get_conf('base'),
+ $conf->_get_build('install_log_dir'),
+ );
+ ### create the dir if it doesn't exit yet
+ $cb->_mkdir( dir => $dir ) unless -d $dir;
+
+ my $file = File::Spec->catfile(
+ $dir,
+ INSTALL_LOG_FILE->( $mod )
+ );
+ if ( open my $fh, ">$file" ) {
+ my $stack = CPANPLUS::Error->stack_as_string;
+ ### remove everything in the log that was there *before*
+ ### we started this install
+ substr( $stack, 0, $log_length, '' );
+
+ print $fh $stack;
+ close $fh;
+
+ print loc("*** Install log written to:\n %1\n\n", $file);
+ } else {
+ warn "Could not open '$file': $!\n";
+ next;
+ }
+ }
+ }
+
+ my $flag;
+ ### then report whether all this went ok or not ###
+ for my $mod (@$mods) {
+ # if( $mod->status->installed ) {
+ if( $status->{$mod} ) {
+ print loc("Module '%1' %tense(%2,past) successfully\n",
+ $mod->module, $action)
+ } else {
+ $flag++;
+ print loc("Error %tense(%1,present) '%2'\n",
+ $action, $mod->module);
+ }
+ }
+
+
+
+ if( !$flag ) {
+ print loc("No errors %tense(%1,present) all modules", $action), "\n";
+ } else {
+ print loc("Problem %tense(%1,present) one or more modules", $action);
+ print "\n";
+ print loc("*** You can view the complete error buffer by pressing '%1' ***\n", 'p')
+ unless $conf->get_conf('verbose') || $self->noninteractive;
+ }
+ print "\n";
+
+ return !$flag;
+}
+
+sub __ask_about_install {
+ my $mod = shift or return;
+ my $prereq = shift or return;
+ my $term = $Shell->term;
+
+ print "\n";
+ print loc( "Module '%1' requires '%2' to be installed",
+ $mod->module, $prereq->module );
+ print "\n\n";
+ print loc( "If you don't wish to see this question anymore\n".
+ "you can disable it by entering the following ".
+ "commands on the prompt:\n '%1'",
+ 's conf prereqs 1; s save' );
+ print "\n\n";
+
+ my $bool = $term->ask_yn(
+ prompt => loc("Should I install this module?"),
+ default => 'y'
+ );
+
+ return $bool;
+}
+
+sub __ask_about_send_test_report {
+ my($mod, $grade) = @_;
+ return 1 unless $grade eq GRADE_FAIL;
+
+ my $term = $Shell->term;
+
+ print "\n";
+ print loc( "Test report prepared for module '%1'.\n Would you like to ".
+ "send it? (You can edit it if you like)", $mod->module );
+ print "\n\n";
+ my $bool = $term->ask_yn(
+ prompt => loc("Would you like to send the test report?"),
+ default => 'n'
+ );
+
+ return $bool;
+}
+
+sub __ask_about_edit_test_report {
+ my($mod, $grade) = @_;
+ return 0 unless $grade eq GRADE_FAIL;
+
+ my $term = $Shell->term;
+
+ print "\n";
+ print loc( "Test report prepared for module '%1'. You can edit this ".
+ "report if you would like", $mod->module );
+ print "\n\n";
+ my $bool = $term->ask_yn(
+ prompt => loc("Would you like to edit the test report?"),
+ default => 'y'
+ );
+
+ return $bool;
+}
+
+
+
+sub _details {
+ my $self = shift;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+ my %hash = @_;
+
+ my $args; my $mods; my $opts;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ modules => { required => 1, store => \$mods },
+ options => { default => { }, store => \$opts },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ ### every module has about 10 lines of details
+ ### maybe more later with Module::CPANTS etc
+ $self->_pager_open if scalar @$mods * 10 > $self->_term_rowcount;
+
+
+ my $format = "%-30s %-30s\n";
+ for my $mod (@$mods) {
+ my $href = $mod->details( %$opts );
+ my @list = sort { $a->module cmp $b->module } $mod->contains;
+
+ unless( $href ) {
+ print loc("No details for %1 - it might be outdated.",
+ $mod->module), "\n";
+ next;
+
+ } else {
+ print loc( "Details for '%1'\n", $mod->module );
+ for my $item ( sort keys %$href ) {
+ printf $format, $item, $href->{$item};
+ }
+
+ my $showed;
+ for my $item ( @list ) {
+ printf $format, ($showed ? '' : 'Contains:'), $item->module;
+ $showed++;
+ }
+ print "\n";
+ }
+ }
+ $self->_pager_close;
+ print "\n";
+
+ return 1;
+}
+
+sub _print {
+ my $self = shift;
+ my %hash = @_;
+
+ my $args; my $opts; my $file;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ input => { default => '', store => \$file },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ my $old; my $fh;
+ if( $file ) {
+ $fh = FileHandle->new( ">$file" )
+ or( warn loc("Could not open '%1': '%2'", $file, $!),
+ return
+ );
+ $old = select $fh;
+ }
+
+
+ $self->_pager_open if !$file;
+
+ print CPANPLUS::Error->stack_as_string;
+
+ $self->_pager_close;
+
+ select $old if $old;
+ print "\n";
+
+ return 1;
+}
+
+sub _set_conf {
+ my $self = shift;
+ my %hash = @_;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+
+ ### possible options
+ ### XXX hard coded, not optimal :(
+ my @types = qw[reconfigure save edit program conf mirrors selfupdate];
+
+
+ my $args; my $opts; my $input;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ input => { default => '', store => \$input },
+ };
+
+ $args = check( $tmpl, \%hash ) or return;
+ }
+
+ my ($type,$key,$value) = $input =~ m/(\w+)\s*(\w*)\s*(.*?)\s*$/;
+ $type = lc $type;
+
+ if( $type eq 'reconfigure' ) {
+ my $setup = CPANPLUS::Configure::Setup->new(
+ configure_object => $conf,
+ term => $self->term,
+ backend => $cb,
+ );
+ return $setup->init;
+
+ } elsif ( $type eq 'save' ) {
+ my $where = {
+ user => CONFIG_USER,
+ system => CONFIG_SYSTEM,
+ }->{ $key } || CONFIG_USER;
+
+ my $rv = $cb->configure_object->save( $where );
+
+ print $rv
+ ? loc("Configuration successfully saved to %1\n", $where)
+ : loc("Failed to save configuration\n" );
+ return $rv;
+
+ } elsif ( $type eq 'edit' ) {
+
+ my $editor = $conf->get_program('editor')
+ or( print(loc("No editor specified")), return );
+
+ my $where = {
+ user => CONFIG_USER,
+ system => CONFIG_SYSTEM,
+ }->{ $key } || CONFIG_USER;
+
+ my $file = $conf->_config_pm_to_file( $where );
+ system("$editor $file");
+
+ ### now reload it
+ ### disable warnings for this
+ { require Module::Loaded;
+ Module::Loaded::mark_as_unloaded( $_ ) for $conf->configs;
+
+ ### reinitialize the config
+ local $^W;
+ $conf->init;
+ }
+
+ return 1;
+
+ } elsif ( $type eq 'mirrors' ) {
+
+ print loc("Readonly list of mirrors (in order of preference):\n\n" );
+
+ my $i;
+ for my $host ( @{$conf->get_conf('hosts')} ) {
+ my $uri = $cb->_host_to_uri( %$host );
+
+ $i++;
+ print "\t[$i] $uri\n";
+ }
+
+ } elsif ( $type eq 'selfupdate' ) {
+ my %valid = map { $_ => $_ }
+ qw|core dependencies enabled_features features all|;
+
+ unless( $valid{$key} ) {
+ print loc( "To update your current CPANPLUS installation, ".
+ "choose one of the these options:\n%1",
+ (join $/, map {"\ts selfupdate $_"} sort keys %valid) );
+ } else {
+ print loc( "Updating your CPANPLUS installation\n" );
+ $cb->selfupdate_object->selfupdate(
+ update => $key,
+ latest => 1,
+ %$opts
+ );
+ }
+
+ } else {
+
+ if ( $type eq 'program' or $type eq 'conf' ) {
+
+ my $format = {
+ conf => '%-25s %s',
+ program => '%-12s %s',
+ }->{ $type };
+
+ unless( $key ) {
+ my @list = grep { $_ ne 'hosts' }
+ $conf->options( type => $type );
+
+ my $method = 'get_' . $type;
+
+ local $Data::Dumper::Indent = 0;
+ for my $name ( @list ) {
+ my $val = $conf->$method($name) || '';
+ ($val) = ref($val)
+ ? (Data::Dumper::Dumper($val) =~ /= (.*);$/)
+ : "'$val'";
+ printf " $format\n", $name, $val;
+ }
+
+ } elsif ( $key eq 'hosts' ) {
+ print loc( "Setting hosts is not trivial.\n" .
+ "It is suggested you use '%1' and edit the " .
+ "configuration file manually", 's edit');
+ } else {
+ my $method = 'set_' . $type;
+ $conf->$method( $key => defined $value ? $value : '' )
+ and print loc("Key '%1' was set to '%2'", $key,
+ defined $value ? $value : 'EMPTY STRING');
+ }
+
+ } else {
+ print loc("Unknown type '%1'",$type || 'EMPTY' );
+ print $/;
+ print loc("Try one of the following:");
+ print $/, join $/, map { "\t'$_'" } sort @types;
+ }
+ }
+ print "\n";
+ return 1;
+}
+
+sub _uptodate {
+ my $self = shift;
+ my %hash = @_;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+
+ my $opts; my $mods;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ modules => { required => 1, store => \$mods },
+ };
+
+ check( $tmpl, \%hash ) or return;
+ }
+
+ ### long listing? short is default ###
+ my $long = $opts->{'long'} ? 1 : 0;
+
+ my @list = scalar @$mods ? @$mods : @{$cb->_all_installed};
+
+ my @rv; my %seen;
+ for my $mod (@list) {
+ ### skip this mod if it's up to date ###
+ next if $mod->is_uptodate;
+ ### skip this mod if it's core ###
+ next if $mod->package_is_perl_core;
+
+ if( $long or !$seen{$mod->package}++ ) {
+ push @rv, $mod;
+ }
+ }
+
+ @rv = sort { $a->module cmp $b->module } @rv;
+
+ $self->cache([undef,@rv]);
+
+ $self->_pager_open if scalar @rv >= $self->_term_rowcount;
+
+ my $format = "%5s %12s %12s %-36s %-10s\n";
+
+ my $i = 1;
+ for my $mod ( @rv ) {
+ printf $format,
+ $i,
+ $self->_format_version($mod->installed_version) || 'Unparsable',
+ $self->_format_version( $mod->version ),
+ $mod->module,
+ $mod->author->cpanid();
+ $i++;
+ }
+ $self->_pager_close;
+
+ return 1;
+}
+
+sub _autobundle {
+ my $self = shift;
+ my %hash = @_;
+ my $cb = $self->backend;
+ my $conf = $cb->configure_object;
+
+ my $opts; my $input;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ input => { default => '', store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+ }
+
+ $opts->{'path'} = $input if $input;
+
+ my $where = $cb->autobundle( %$opts );
+
+ print $where
+ ? loc("Wrote autobundle to '%1'", $where)
+ : loc("Could not create autobundle" );
+ print "\n";
+
+ return $where ? 1 : 0;
+}
+
+sub _uninstall {
+ my $self = shift;
+ my %hash = @_;
+ my $cb = $self->backend;
+ my $term = $self->term;
+ my $conf = $cb->configure_object;
+
+ my $opts; my $mods;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ modules => { default => [], store => \$mods },
+ };
+
+ check( $tmpl, \%hash ) or return;
+ }
+
+ my $force = $opts->{'force'} || $conf->get_conf('force');
+
+ unless( $force ) {
+ my $list = join "\n", map { ' ' . $_->module } @$mods;
+
+ print loc("
+This will uninstall the following modules:
+%1
+
+Note that if you installed them via a package manager, you probably
+should use the same package manager to uninstall them
+
+", $list);
+
+ return unless $term->ask_yn(
+ prompt => loc("Are you sure you want to continue?"),
+ default => 'n',
+ );
+ }
+
+ ### first loop over all the modules to uninstall them ###
+ for my $mod (@$mods) {
+ print loc("Uninstalling '%1'", $mod->module), "\n";
+
+ $mod->uninstall( %$opts );
+ }
+
+ my $flag;
+ ### then report whether all this went ok or not ###
+ for my $mod (@$mods) {
+ if( $mod->status->uninstall ) {
+ print loc("Module '%1' %tense(uninstall,past) successfully\n",
+ $mod->module )
+ } else {
+ $flag++;
+ print loc("Error %tense(uninstall,present) '%1'\n", $mod->module);
+ }
+ }
+
+ if( !$flag ) {
+ print loc("All modules %tense(uninstall,past) successfully"), "\n";
+ } else {
+ print loc("Problem %tense(uninstalling,present) one or more modules" ),
+ "\n";
+ print loc("*** You can view the complete error buffer by pressing '%1'".
+ "***\n", 'p') unless $conf->get_conf('verbose');
+ }
+ print "\n";
+
+ return !$flag;
+}
+
+sub _reports {
+ my $self = shift;
+ my %hash = @_;
+ my $cb = $self->backend;
+ my $term = $self->term;
+ my $conf = $cb->configure_object;
+
+ my $opts; my $mods;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ modules => { default => '', store => \$mods },
+ };
+
+ check( $tmpl, \%hash ) or return;
+ }
+
+ ### XXX might need to be conditional ###
+ $self->_pager_open;
+
+ for my $mod (@$mods) {
+ my @list = $mod->fetch_report( %$opts )
+ or( print(loc("No reports available for this distribution.")),
+ next
+ );
+
+ @list = reverse
+ map { $_->[0] }
+ sort { $a->[1] cmp $b->[1] }
+ map { [$_, $_->{'dist'}.':'.$_->{'platform'}] } @list;
+
+
+
+ ### XXX this may need to be sorted better somehow ###
+ my $url;
+ my $format = "%8s %s %s\n";
+
+ my %seen;
+ for my $href (@list ) {
+ print "[" . $mod->author->cpanid .'/'. $href->{'dist'} . "]\n"
+ unless $seen{ $href->{'dist'} }++;
+
+ printf $format, $href->{'grade'}, $href->{'platform'},
+ ($href->{'details'} ? '(*)' : '');
+
+ $url ||= $href->{'details'};
+ }
+
+ print "\n==> $url\n" if $url;
+ print "\n";
+ }
+ $self->_pager_close;
+
+ return 1;
+}
+
+
+### Load plugins
+{ my @PluginModules;
+ my %Dispatch = (
+ showtip => [ __PACKAGE__, '_show_random_tip'],
+ plugins => [ __PACKAGE__, '_list_plugins' ],
+ '?' => [ __PACKAGE__, '_plugins_usage' ],
+ );
+
+ sub plugin_modules { return @PluginModules }
+ sub plugin_table { return %Dispatch }
+
+ ### find all plugins first
+ if( check_install( module => 'Module::Pluggable', version => '2.4') ) {
+ require Module::Pluggable;
+
+ my $only_re = __PACKAGE__ . '::Plugins::\w+$';
+
+ Module::Pluggable->import(
+ sub_name => '_plugins',
+ search_path => __PACKAGE__,
+ only => qr/$only_re/,
+ #except => [ INSTALLER_MM, INSTALLER_SAMPLE ]
+ );
+
+ push @PluginModules, __PACKAGE__->_plugins;
+ }
+
+ ### now try to load them
+ for my $p ( __PACKAGE__->plugin_modules ) {
+ my %map = eval { load $p; $p->import; $p->plugins };
+ error(loc("Could not load plugin '$p': $@")), next if $@;
+
+ ### register each plugin
+ while( my($name, $func) = each %map ) {
+
+ if( not length $name or not length $func ) {
+ error(loc("Empty plugin name or dispatch function detected"));
+ next;
+ }
+
+ if( exists( $Dispatch{$name} ) ) {
+ error(loc("'%1' is already registered by '%2'",
+ $name, $Dispatch{$name}->[0]));
+ next;
+ }
+
+ ### register name, package and function
+ $Dispatch{$name} = [ $p, $func ];
+ }
+ }
+
+ ### dispatch a plugin command to it's function
+ sub _meta {
+ my $self = shift;
+ my %hash = @_;
+ my $cb = $self->backend;
+ my $term = $self->term;
+ my $conf = $cb->configure_object;
+
+ my $opts; my $input;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ options => { default => { }, store => \$opts },
+ input => { default => '', store => \$input },
+ };
+
+ check( $tmpl, \%hash ) or return;
+ }
+
+ $input =~ s/\s*(\S+)\s*//;
+ my $cmd = $1;
+
+ ### look up the command, or go to the default
+ my $aref = $Dispatch{ $cmd } || [ __PACKAGE__, '_plugin_default' ];
+
+ my($pkg,$func) = @$aref;
+
+ my $rv = eval { $pkg->$func( $self, $cb, $cmd, $input, $opts ) };
+
+ error( $@ ) if $@;
+
+ ### return $rv instead, so input loop can be terminated?
+ return 1;
+ }
+
+ sub _plugin_default { error(loc("No such plugin command")) }
+}
+
+### plugin commands
+{ my $help_format = " /%-20s # %s\n";
+
+ sub _list_plugins {
+ print loc("Available plugins:\n");
+ print loc(" List usage by using: /? PLUGIN_NAME\n" );
+ print $/;
+
+ my %table = __PACKAGE__->plugin_table;
+ for my $name( sort keys %table ) {
+ my $pkg = $table{$name}->[0];
+ my $this = __PACKAGE__;
+
+ my $who = $pkg eq $this
+ ? "Standard Plugin"
+ : do { $pkg =~ s/^$this/../; "Provided by: $pkg" };
+
+ printf $help_format, $name, $who;
+ }
+
+ print $/.$/;
+
+ print " Write your own plugins? Read the documentation of:\n" .
+ " CPANPLUS::Shell::Default::Plugins::HOWTO\n";
+
+ print $/;
+ }
+
+ sub _list_plugins_help {
+ return sprintf $help_format, 'plugins', loc("lists available plugins");
+ }
+
+ ### registered as a plugin too
+ sub _show_random_tip_help {
+ return sprintf $help_format, 'showtip', loc("show usage tips" );
+ }
+
+ sub _plugins_usage {
+ my $pkg = shift;
+ my $shell = shift;
+ my $cb = shift;
+ my $cmd = shift;
+ my $input = shift;
+ my %table = __PACKAGE__->plugin_table;
+
+ my @list = length $input ? split /\s+/, $input : sort keys %table;
+
+ for my $name( @list ) {
+
+ ### no such plugin? skip
+ error(loc("No such plugin '$name'")), next unless $table{$name};
+
+ my $pkg = $table{$name}->[0];
+ my $func = $table{$name}->[1] . '_help';
+
+ if ( my $sub = $pkg->can( $func ) ) {
+ eval { print $sub->() };
+ error( $@ ) if $@;
+
+ } else {
+ print " No usage for '$name' -- try perldoc $pkg";
+ }
+
+ print $/;
+ }
+
+ print $/.$/;
+ }
+
+ sub _plugins_usage_help {
+ return sprintf $help_format, '? [NAME ...]',
+ loc("show usage for plugins");
+ }
+}
+
+### send a command to a remote host, retrieve the answer;
+sub __send_remote_command {
+ my $self = shift;
+ my $cmd = shift;
+ my $remote = $self->remote or return;
+ my $user = $remote->{'username'};
+ my $pass = $remote->{'password'};
+ my $conn = $remote->{'connection'};
+ my $end = "\015\012";
+ my $answer;
+
+ my $send = join "\0", $user, $pass, $cmd;
+
+ print $conn $send . $end;
+
+ ### XXX why doesn't something like this just work?
+ #1 while recv($conn, $answer, 1024, 0);
+ while(1) {
+ my $buff;
+ $conn->recv( $buff, 1024, 0 );
+ $answer .= $buff;
+ last if $buff =~ /$end$/;
+ }
+
+ my($status,$buffer) = split "\0", $answer;
+
+ return ($status, $buffer);
+}
+
+
+sub _read_configuration_from_rc {
+ my $rc_file = shift;
+
+ my $href;
+ if( can_load( modules => { 'Config::Auto' => '0.0' } ) ) {
+ $Config::Auto::DisablePerl = 1;
+
+ eval { $href = Config::Auto::parse( $rc_file, format => 'space' ) };
+
+ print loc( "Unable to read in config file '%1': %2",
+ $rc_file, $@ ) if $@;
+ }
+
+ return $href || {};
+}
+
+{ my @tips = (
+ loc( "You can update CPANPLUS by running: '%1'", 's selfupdate' ),
+ loc( "You can install modules by URL using '%1'", 'i URL' ),
+ loc( "You can turn off these tips using '%1'",
+ 's conf show_startup_tip 0' ),
+ loc( "You can use wildcards like '%1' and '%2' on search results",
+ '*', '..' ),
+ loc( "You can use plugins. Type '%1' to list available plugins",
+ '/plugins' ),
+ loc( "You can show all your out of date modules using '%1'", 'o' ),
+ loc( "Many operations take options, like '%1' or '%2'",
+ '--verbose', '--skiptest' ),
+ loc( "The documentation in %1 and %2 is very useful",
+ "CPANPLUS::Module", "CPANPLUS::Backend" ),
+ loc( "You can type '%1' for help and '%2' to exit", 'h', 'q' ),
+ );
+
+ sub _show_random_tip {
+ my $self = shift;
+ print $/, "Did you know...\n ", $tips[ int rand scalar @tips ], $/;
+ return 1;
+ }
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Shell::Classic>, L<CPANPLUS::Shell>, L<cpanp>
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
+__END__
+
+TODO:
+ e => "_expand_inc", # scratch it, imho -- not used enough
+
+### free letters: g j k n y ###
diff --git a/lib/CPANPLUS/Shell/Default/Plugins/HOWTO.pod b/lib/CPANPLUS/Shell/Default/Plugins/HOWTO.pod
new file mode 100644
index 0000000000..c537c4ea72
--- /dev/null
+++ b/lib/CPANPLUS/Shell/Default/Plugins/HOWTO.pod
@@ -0,0 +1,136 @@
+=head1 NAME
+
+CPANPLUS::Shell::Default::Plugins::HOWTO -- documentation on how to write your own plugins
+
+=head1 SYNOPSIS
+
+ package CPANPLUS::Shell::Default::Plugins::MyPlugin;
+
+ ### return command => method mapping
+ sub plugins { ( myplugin1 => 'mp1', myplugin2 => 'mp2' ) }
+
+ ### method called when the command '/myplugin1' is issued
+ sub mp1 { .... }
+
+ ### method called when the command '/? myplugin1' is issued
+ sub mp1_help { return "Help Text" }
+
+=head1 DESCRIPTION
+
+This pod text explains how to write your own plugins for
+C<CPANPLUS::Shell::Default>.
+
+=head1 HOWTO
+
+=head2 Registering Plugin Modules
+
+Plugins are detected by using C<Module::Pluggable>. Every module in
+the C<CPANPLUS::Shell::Default::Plugins::*> namespace is considered a
+plugin, and is attempted to be loaded.
+
+Therefor, any plugin must be declared in that namespace, in a corresponding
+C<.pm> file.
+
+=head2 Registering Plugin Commands
+
+To register any plugin commands, a list of key value pairs must be returned
+by a C<plugins> method in your package. The keys are the commands you wish
+to register, the values are the methods in the plugin package you wish to have
+called when the command is issued.
+
+For example, a simple 'Hello, World!' plugin:
+
+ package CPANPLUS::Shell::Default::Plugins::HW;
+
+ sub plugins { return ( helloworld => 'hw' ) };
+
+ sub hw { print "Hello, world!\n" }
+
+When the user in the default shell now issues the C</helloworld> command,
+this command will be dispatched to the plugin, and it's C<hw> method will
+be called
+
+=head2 Registering Plugin Help
+
+To provide usage information for your plugin, the user of the default shell
+can type C</? PLUGIN_COMMAND>. In that case, the function C<PLUGIN_COMMAND_help>
+will be called in your plugin package.
+
+For example, extending the above example, when a user calls C</? helloworld>,
+the function C<hw_help> will be called, which might look like this:
+
+ sub hw_help { " /helloworld # prints "Hello, world!\n" }
+
+If you dont provide a corresponding _help function to your commands, the
+default shell will handle it gracefully, but the user will be stuck without
+usage information on your commands, so it's considered undesirable to omit
+the help functions.
+
+=head2 Arguments to Plugin Commands
+
+Any plugin function will receive the following arguments when called, which
+are all positional:
+
+=over 4
+
+=item Classname -- The name of your plugin class
+
+=item Shell -- The CPANPLUS::Shell::Default object
+
+=item Backend -- The CPANPLUS::Backend object
+
+=item Command -- The command issued by the user
+
+=item Input -- The input string from the user
+
+=item Options -- A hashref of options provided by the user
+
+=back
+
+For example, the following command:
+
+ /helloworld bob --nofoo --bar=2 joe
+
+Would yield the following arguments:
+
+ sub hw {
+ my $class = shift; # CPANPLUS::Shell::Default::Plugins::HW
+ my $shell = shift; # CPANPLUS::Shell::Default object
+ my $cb = shift; # CPANPLUS::Backend object
+ my $cmd = shift; # 'helloworld'
+ my $input = shift; # 'bob joe'
+ my $opts = shift; # { foo => 0, bar => 2 }
+
+ ....
+ }
+
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Shell::Default>, L<CPANPLUS::Shell>, L<cpanp>
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/Shell/Default/Plugins/Remote.pm b/lib/CPANPLUS/Shell/Default/Plugins/Remote.pm
new file mode 100644
index 0000000000..c351367da0
--- /dev/null
+++ b/lib/CPANPLUS/Shell/Default/Plugins/Remote.pm
@@ -0,0 +1,188 @@
+package CPANPLUS::Shell::Default::Plugins::Remote;
+
+use strict;
+
+use Module::Load;
+use Params::Check qw[check];
+use CPANPLUS::Error qw[error msg];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+=head1 NAME
+
+CPANPLUS::Shell::Default::Plugins::Remote
+
+=head1 SYNOPSIS
+
+ CPAN Terminal> /connect localhost 1337 --user=foo --pass=bar
+ ...
+ CPAN Terminal@localhost> /disconnect
+
+=head1 DESCRIPTION
+
+This is a C<CPANPLUS::Shell::Default> plugin that allows you to connect
+to a machine running an instance of C<CPANPLUS::Daemon>, allowing remote
+usage of the C<CPANPLUS Shell>.
+
+A sample session, updating all modules on a remote machine, might look
+like this:
+
+ CPAN Terminal> /connect --user=my_user --pass=secret localhost 1337
+
+ Connection accepted
+
+ Successfully connected to 'localhost' on port '11337'
+
+ Note that no output will appear until a command has completed
+ -- this may take a while
+
+
+ CPAN Terminal@localhost> o; i *
+
+ [....]
+
+ CPAN Terminal@localhost> /disconnect
+
+ CPAN Terminal>
+
+=cut
+
+### store the original prompt here, so we can restore it on disconnect
+my $Saved_Prompt;
+
+sub plugins { ( connect => 'connect', disconnect => 'disconnect' ) }
+
+sub connect {
+ my $class = shift;
+ my $shell = shift;
+ my $cb = shift;
+ my $cmd = shift;
+ my $input = shift || '';
+ my $opts = shift || {};
+ my $conf = $cb->configure_object;
+
+ my $user; my $pass;
+ { local $Params::Check::ALLOW_UNKNOWN = 1;
+
+ my $tmpl = {
+ user => { default => $conf->_get_daemon('username'),
+ store => \$user },
+ pass => { default => $conf->_get_daemon('password'),
+ store => \$pass },
+ };
+
+ check( $tmpl, $opts ) or return;
+ }
+
+ my @parts = split /\s+/, $input;
+ my $host = shift @parts || 'localhost';
+ my $port = shift @parts || $conf->_get_daemon('port');
+
+ load IO::Socket;
+
+ my $remote = IO::Socket::INET->new(
+ Proto => "tcp",
+ PeerAddr => $host,
+ PeerPort => $port,
+ ) or (
+ error( loc( "Cannot connect to port '%1' ".
+ "on host '%2'", $port, $host ) ),
+ return
+ );
+
+ my $con = {
+ connection => $remote,
+ username => $user,
+ password => $pass,
+ };
+
+ ### store the connection
+ $shell->remote( $con );
+
+ my($status,$buffer) = $shell->__send_remote_command(
+ "VERSION=$CPANPLUS::Shell::Default::VERSION");
+
+ if( $status ) {
+ print "\n$buffer\n\n";
+
+ print loc( "Successfully connected to '%1' on port '%2'",
+ $host, $port );
+ print "\n\n";
+ print loc( "Note that no output will appear until a command ".
+ "has completed\n-- this may take a while" );
+ print "\n\n";
+
+ ### save the original prompt
+ $Saved_Prompt = $shell->prompt;
+
+ $shell->prompt( $shell->brand .'@'. $host .'> ' );
+
+ } else {
+ print "\n$buffer\n\n";
+
+ print loc( "Failed to connect to '%1' on port '%2'",
+ $host, $port );
+ print "\n\n";
+
+ $shell->remote( undef );
+ }
+}
+
+sub disconnect {
+ my $class = shift;
+ my $shell = shift;
+
+ print "\n", ( $shell->remote
+ ? loc( "Disconnecting from remote host" )
+ : loc( "Not connected to remote host" )
+ ), "\n\n";
+
+ $shell->remote( undef );
+ $shell->prompt( $Saved_Prompt );
+}
+
+sub connect_help {
+ return loc(
+ " /connect [HOST PORT] # Connect to the remote machine,\n" .
+ " # defaults taken from your config\n" .
+ " --user=USER # Optional username\n" .
+ " --pass=PASS # Optional password" );
+}
+
+sub disconnect_help {
+ return loc(
+ " /disconnect # Disconnect from the remote server" );
+}
+
+1;
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Shell::Default>, L<CPANPLUS::Shell>, L<cpanp>
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/Shell/Default/Plugins/Source.pm b/lib/CPANPLUS/Shell/Default/Plugins/Source.pm
new file mode 100644
index 0000000000..889b3d3d9b
--- /dev/null
+++ b/lib/CPANPLUS/Shell/Default/Plugins/Source.pm
@@ -0,0 +1,107 @@
+package CPANPLUS::Shell::Default::Plugins::Source;
+
+use strict;
+use CPANPLUS::Error qw[error msg];
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+=head1 NAME
+
+CPANPLUS::Shell::Default::Plugins::Source
+
+=head1 SYNOPSIS
+
+ CPAN Terminal> /source /tmp/list_of_commands /tmp/more_commands
+
+=head1 DESCRIPTION
+
+This is a C<CPANPLUS::Shell::Default> plugin that works just like
+your unix shells source(1) command; it reads in a file that has
+commands in it to execute, and then executes them.
+
+A sample file might look like this:
+
+ # first, update all the source files
+ x --update_source
+
+ # find all of my modules that are on the CPAN
+ # test them, and store the error log
+ a ^KANE$'
+ t *
+ p /home/kane/cpan-autotest/log
+
+ # and inform us we're good to go
+ ! print "Autotest complete, log stored; please enter your commands!"
+
+Note how empty lines, and lines starting with a '#' are being skipped
+in the execution.
+
+=cut
+
+
+sub plugins { return ( source => 'source' ) }
+
+sub source {
+ my $class = shift;
+ my $shell = shift;
+ my $cb = shift;
+ my $cmd = shift;
+ my $input = shift || '';
+ my $opts = shift || {};
+ my $verbose = $cb->configure_object->get_conf('verbose');
+
+ for my $file ( split /\s+/, $input ) {
+ my $fh = FileHandle->new("$file") or(
+ error(loc("Could not open file '%1': %2", $file, $!)),
+ next
+ );
+
+ while( my $line = <$fh> ) {
+ chomp $line;
+
+ next if $line !~ /\S+/; # skip empty/whitespace only lines
+ next if $line =~ /^#/; # skip comments
+
+ msg(loc("Dispatching '%1'", $line), $verbose);
+ return 1 if $shell->dispatch_on_input( input => $line );
+ }
+ }
+}
+
+sub source_help {
+ return loc(' /source FILE [FILE ..] '.
+ '# read in commands from the specified file' ),
+}
+
+1;
+
+=pod
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Shell::Default>, L<CPANPLUS::Shell>, L<cpanp>
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/bin/cpan2dist b/lib/CPANPLUS/bin/cpan2dist
new file mode 100644
index 0000000000..2fff7563dc
--- /dev/null
+++ b/lib/CPANPLUS/bin/cpan2dist
@@ -0,0 +1,597 @@
+#!/usr/bin/perl -w
+use strict;
+use CPANPLUS::Backend;
+use CPANPLUS::Dist;
+use CPANPLUS::Internals::Constants;
+use Data::Dumper;
+use Getopt::Long;
+use File::Spec;
+use File::Basename;
+use Locale::Maketext::Simple Class => 'CPANPLUS', Style => 'gettext';
+
+use constant PREREQ_SKIP_CLASS => 'CPANPLUS::To::Dist::PREREQ_SKIP';
+use constant ALARM_CLASS => 'CPANPLUS::To::Dist::ALARM';
+
+### print when you can
+$|++;
+
+my $cb = CPANPLUS::Backend->new
+ or die loc("Could not create new CPANPLUS::Backend object");
+my $conf = $cb->configure_object;
+
+my %formats = map { $_ => $_ } CPANPLUS::Dist->dist_types;
+
+my $opts = {};
+GetOptions( $opts,
+ 'format=s', 'archive',
+ 'verbose!', 'force!',
+ 'skiptest!', 'keepsource!',
+ 'makefile!', 'buildprereq!',
+ 'help', 'flushcache',
+ 'ban=s@', 'banlist=s@',
+ 'ignore=s@', 'ignorelist=s@',
+ 'defaults', 'modulelist=s@',
+ 'logfile=s', 'timeout=s',
+ 'dist-opts=s%',
+ 'default-banlist!',
+ 'default-ignorelist!',
+ );
+
+die usage() if exists $opts->{'help'};
+
+### parse options
+my $tarball = $opts->{'archive'} || 0;
+my $keep = $opts->{'keepsource'} ? 1 : 0;
+my $prereqbuild = exists $opts->{'buildprereq'}
+ ? $opts->{'buildprereq'}
+ : 0;
+my $timeout = exists $opts->{'timeout'}
+ ? $opts->{'timeout'}
+ : 300;
+
+### use default answers?
+$ENV{'PERL_MM_USE_DEFAULT'} = $opts->{'defaults'} ? 1 : 0;
+
+my $format;
+### if provided, we go with the command line option, fall back to conf setting
+{ $format = $opts->{'format'} || $conf->get_conf('dist_type');
+ $conf->set_conf( dist_type => $format );
+
+ ### is this a valid format??
+ die loc("Invalid format: " . ($format || "[NONE]") ) . usage()
+ unless $formats{$format};
+
+ my %map = ( verbose => 'verbose',
+ force => 'force',
+ skiptest => 'skiptest',
+ makefile => 'prefer_makefile'
+ );
+
+ ### set config options from arguments
+ while (my($key,$val) = each %map) {
+ my $bool = exists $opts->{$key} ? $opts->{$key} : $conf->get_conf($val);
+ $conf->set_conf( $val => $bool );
+ }
+}
+
+my @modules = @ARGV;
+if( exists $opts->{'modulelist'} ) {
+ push @modules, map { parse_file( $_ ) } @{ $opts->{'modulelist'} };
+}
+
+die usage() unless @modules;
+
+
+my $fh;
+LOGFILE: {
+ if( my $file = $opts->{logfile} ) {
+ open $fh, ">$file" or (
+ warn loc("Could not open '%1' for writing: %2", $file,$!),
+ last LOGFILE
+ );
+
+ warn "Logging to '$file'\n";
+
+ *STDERR = $fh;
+ *STDOUT = $fh;
+ }
+}
+
+### reload indices if so desired
+$cb->reload_indices() if $opts->{'flushcache'};
+
+{ my @ban = exists $opts->{'ban'}
+ ? map { qr/$_/ } @{ $opts->{'ban'} }
+ : ();
+
+
+ if( exists $opts->{'banlist'} ) {
+ push @ban, map { parse_file( $_, 1 ) } @{ $opts->{'banlist'} };
+ }
+
+ push @ban, map { s/\s+//; $_ }
+ map { [split /\s*#\s*/]->[0] }
+ grep { /#/ }
+ map { split /\n/ } _default_ban_list()
+ if $opts->{'default-banlist'};
+
+ ### use our prereq install callback
+ $conf->set_conf( prereqs => PREREQ_ASK );
+
+ ### register install callback ###
+ $cb->_register_callback(
+ name => 'install_prerequisite',
+ code => \&__ask_about_install,
+ );
+
+
+ ### check for ban patterns when handling prereqs
+ sub __ask_about_install {
+
+ my $mod = shift or return;
+ my $prereq = shift or return;
+
+
+ ### die with an error object, so we can verify that
+ ### the die came from this location, and that it's an
+ ### 'acceptable' death
+ my $pat = ban_me( $prereq );
+ die bless \(loc("Module '%1' requires '%2' to be installed " .
+ "but found in your ban list (%3) -- skipping",
+ $mod->module, $prereq->module, $pat )),
+ PREREQ_SKIP_CLASS if $pat;
+ return 1;
+ }
+
+ ### should we skip this module?
+ sub ban_me {
+ my $mod = shift;
+
+ for my $pat ( @ban ) {
+ return $pat if $mod->module =~ /$pat/;
+ }
+ return;
+ }
+}
+
+### patterns to strip from prereq lists
+{ my @ignore = exists $opts->{'ignore'}
+ ? map { qr/$_/ } @{ $opts->{'ignore'} }
+ : ();
+
+ if( exists $opts->{'ignorelist'} ) {
+ push @ignore, map { parse_file( $_, 1 ) } @{ $opts->{'ignorelist'} };
+ }
+
+ push @ignore, map { s/\s+//; $_ }
+ map { [split /\s*#\s*/]->[0] }
+ grep { /#/ }
+ map { split /\n/ } _default_ignore_list()
+ if $opts->{'default-ignorelist'};
+
+
+ ### register install callback ###
+ $cb->_register_callback(
+ name => 'filter_prereqs',
+ code => \&__filter_prereqs,
+ );
+
+ sub __filter_prereqs {
+ my $cb = shift;
+ my $href = shift;
+
+ for my $name ( keys %$href ) {
+ my $obj = $cb->parse_module( module => $name ) or (
+ warn "Cannot make a module object out of ".
+ "'$name' -- skipping\n",
+ next );
+
+ if( my $pat = ignore_me( $obj ) ) {
+ warn loc("'%1' found in your ignore list (%2) ".
+ "-- filtering it out\n", $name, $pat);
+
+ delete $href->{ $name };
+ }
+ }
+
+ return $href;
+ }
+
+ ### should we skip this module?
+ sub ignore_me {
+ my $mod = shift;
+
+ for my $pat ( @ignore ) {
+ return $pat if $mod->module =~ /$pat/;
+ return $pat if $mod->package_name =~ /$pat/;
+ }
+ return;
+ }
+}
+
+
+my %done;
+for my $name (@modules) {
+
+ my $obj;
+
+ ### is it a tarball? then we get it locally and transform it
+ ### and it's dependencies into .debs
+ if( $tarball ) {
+ ### make sure we use an absolute path, so chdirs() dont
+ ### mess things up
+ $name = File::Spec->rel2abs( $name );
+
+ ### ENOTARBALL?
+ unless( -e $name ) {
+ warn loc("Archive '$name' does not exist");
+ next;
+ }
+
+ $obj = CPANPLUS::Module::Fake->new(
+ module => basename($name),
+ path => dirname($name),
+ package => basename($name),
+ );
+
+ ### if it's a traditional CPAN package, we can tidy
+ ### up the module name some
+ $obj->module( $obj->package_name ) if $obj->package_name;
+
+ ### get the version from the package name
+ $obj->version( $obj->package_version || 0 );
+
+ ### set the location of the tarball
+ $obj->status->fetch($name);
+
+ ### plain old cpan module?
+ } else {
+
+ ### find the corresponding module object ###
+ $obj = $cb->parse_module( module => $name ) or (
+ warn "Cannot make a module object out of ".
+ "'$name' -- skipping\n",
+ next );
+ }
+
+ ### you banned it?
+ if( my $pat = ban_me( $obj ) ) {
+ warn loc("'%1' found in your ban list (%2) -- skipping\n",
+ $obj->module, $pat );
+ next;
+ }
+
+ ### or just ignored it?
+ if( my $pat = ignore_me( $obj ) ) {
+ warn loc("'%1' found in your ignore list (%2) -- skipping\n",
+ $obj->module, $pat );
+ next;
+ }
+
+
+ my $dist = eval {
+ local $SIG{ALRM} = sub { die bless {}, ALARM_CLASS }
+ if $timeout;
+
+ alarm $timeout || 0;
+
+ my $dist_opts = $opts->{'dist-opts'} || {};
+
+ my $rv = $obj->install(
+ prereq_target => 'create',
+ target => 'create',
+ keep_source => $keep,
+ prereq_build => $prereqbuild,
+
+ ### any passed arbitrary options
+ %$dist_opts,
+ );
+
+ alarm 0;
+
+ $rv;
+ };
+
+ ### set here again, in case the install dies
+ alarm 0;
+
+ ### install failed due to a 'die' in our prereq skipper?
+ if( $@ and ref $@ and $@->isa( PREREQ_SKIP_CLASS ) ) {
+ warn loc("Dist creation of '%1' skipped: '%2'",
+ $obj->module, ${$@} );
+ next;
+
+ } elsif ( $@ and ref $@ and $@->isa( ALARM_CLASS ) ) {
+ warn loc("\nDist creation of '%1' skipped, build time exceeded: ".
+ "%2 seconds\n", $obj->module, $timeout );
+ next;
+
+ ### died for some other reason? just report and skip
+ } elsif ( $@ ) {
+ warn loc("Dist creation of '%1' failed: '%2'",
+ $obj->module, $@ );
+ next;
+ }
+
+ ### we didn't get a dist object back?
+ unless ($dist and $obj->status->dist) {
+ warn loc("Unable to create '%1' dist of '%2'", $format, $obj->module);
+ next
+ }
+
+ print "Created '$format' distribution for ", $obj->module,
+ " to:\n\t", $obj->status->dist->status->dist, "\n";
+}
+
+
+sub parse_file {
+ my $file = shift or return;
+ my $qr = shift() ? 1 : 0;
+
+ my $fh = OPEN_FILE->( $file ) or return;
+
+ my @rv;
+ while( <$fh> ) {
+ chomp;
+ next if /^#/; # skip comments
+ next unless /\S/; # skip empty lines
+ s/^(\S+).*/$1/; # skip extra info
+ push @rv, $qr ? qr/$_/ : $_; # add pattern to the list
+ }
+
+ return @rv;
+}
+
+=head1 NAME
+
+cpan2dist - The CPANPLUS distribution creator
+
+=head1 DESCRIPTION
+
+This script will create distributions of C<CPAN> modules of the format
+you specify, including its prerequisites. These packages can then be
+installed using the corresponding package manager for the format.
+
+Note, you can also do this interactively from the default shell,
+C<CPANPLUS::Shell::Default>. See the C<CPANPLUS::Dist> documentation,
+as well as the documentation of your format of choice for any format
+specific documentation.
+
+=head1 USAGE
+
+=cut
+
+sub usage {
+ my $me = basename($0);
+ my $formats = join "\n", map { "\t\t$_" } sort keys %formats;
+
+ my $usage = << '=cut';
+=pod
+
+ Usage: cpan2dist [--format FMT] [OPTS] Mod::Name [Mod::Name, ...]
+ cpan2dist [--format FMT] [OPTS] --modulelist /tmp/mods.list
+ cpan2dist [--format FMT] [OPTS] --archive /tmp/dist [/tmp/dist2]
+
+ Will create a distribution of type FMT of the modules
+ specified on the command line, and all their prerequisites.
+
+ Can also create a distribution of type FMT from a local
+ archive and all it's prerequisites
+
+=cut
+
+ $usage .= qq[
+ Possible formats are:
+$formats
+
+ You can install more formats from CPAN!
+ \n];
+
+ $usage .= << '=cut';
+=pod
+
+Options:
+
+ ### take no argument:
+ --help Show this help message
+ --skiptest Skip tests. Can be negated using --noskiptest
+ --force Force operation. Can be negated using --noforce
+ --verbose Be verbose. Can be negated using --noverbose
+ --keepsource Keep sources after building distribution. Can be
+ negated by --nokeepsource. May not be supported
+ by all formats
+ --makefile Prefer Makefile.PL over Build.PL. Can be negated
+ using --nomakefile. Defaults to your config setting
+ --buildprereq Build packages of any prerequisites, even if they are
+ already uptodate on the local system. Can be negated
+ using --nobuildprereq. Defaults to false.
+ --archive Indicate that all modules listed are actually archives
+ --flushcache Update CPANPLUS' cache before commencing any operation
+ --defaults Instruct ExtUtils::MakeMaker and Module::Build to use
+ default answers during 'perl Makefile.PL' or 'perl
+ Build.PL' calls where possible
+
+ ### take argument:
+ --format Installer format to use (defaults to config setting)
+ --ban Patterns of module names to skip during installation
+ (affects prerequisites too) May be given multiple times
+ --banlist File containing patterns that could be given to --ban
+ Are appended to the ban list built up by --ban
+ May be given multiple times.
+ --ignore Patterns of modules to exclude from prereq list. Useful
+ for when a prereq listed by a CPAN module is resolved
+ in another way than from its corresponding CPAN package
+ (Match is done on both module name, and package name of
+ the package the module is in)
+ --ignorelist File containing patterns that may be given to --ignore.
+ Are appended to the ban list build up by --ignore.
+ May be given multiple times.
+ --modulelist File containing a list of modules that should be built.
+ Are appended to the list of command line modules.
+ May be given multiple times.
+ --logfile File to log all output to. By default, all output goes
+ to the console.
+ --timeout The allowed time for buliding a distribution before
+ aborting. This is useful to terminate any build that
+ hang or happen to be interactive despite being told not
+ to be. Defaults to 300 seconds. To turn off, you can
+ set it to 0.
+ --dist-opts Arbitrary options passed along to the chosen installer
+ format's prepare()/create() routine.
+
+ ### builtin lists
+ --default-banlist Use our builtin banlist. Works just like --ban
+ and --banlist, but with pre-set lists. See the
+ "Builtin Lists" section for details.
+ --default-ignorelist Use our builtin ignorelist. Works just like
+ --ignore and --ignorelist but with pre-set lists.
+ See the "Builtin Lists" section for details.
+
+Examples:
+
+ ### build a debian package of DBI and it's prerequisites,
+ ### don't bother running tests
+ cpan2dist --format CPANPLUS::Dist::Deb --buildprereq --skiptest DBI
+
+ ### Build a package, whose format is determined by your config of
+ ### the local tarball, reloading cpanplus' indices first and using
+ ### the tarballs Makefile.PL if it has one.
+ cpan2dist --makefile --flushcache --archive /path/to/Cwd-1.0.tgz
+
+ ### build a package from Net::FTP, but dont build any packages or
+ ### dependencies whose name match 'Foo', 'Bar' or any of the
+ ### patterns mentioned in /tmp/ban
+ cpan2dist --ban Foo --ban Bar --banlist /tmp/ban Net::FTP
+
+ ### build a package from Net::FTP, but ignore it's listed dependency
+ ### on IO::Socket, as it's shipped per default with the OS we're on
+ cpan2dist --ignore IO::Socket Net::FTP
+
+ ### building all modules listed, plus their prerequisites
+ cpan2dist --ignorelist /tmp/modules.ignore --banlist /tmp/modules.ban
+ --modulelist /tmp/modules.list --buildprereq --flushcache
+ --makefile --defaults
+
+ ### pass arbitrary options to the format's prepare()/create() routine
+ cpan2dist --dist-opts deb_version=3 --dist-opts prefix=corp
+
+=cut
+
+ $usage .= qq[
+Builtin Lists:
+
+ Ignore list:] . _default_ignore_list() . qq[
+ Ban list:] . _default_ban_list();
+
+ ### strip the pod directives
+ $usage =~ s/=pod\n//g;
+
+ return $usage;
+}
+
+=pod
+
+=head1 Built-In Filter Lists
+
+Some modules you'd rather not package. Some because they
+are part of core-perl and you dont want a new package.
+Some because they won't build on your system. Some because
+your package manager of choice already packages them for you.
+
+There may be a myriad of reasons. You can use the C<--ignore>
+and C<--ban> options for this, but we provide some built-in
+lists that catch common cases. You can use these built-in lists
+if you like, or supply your own if need be.
+
+=head2 Built-In Ignore List
+
+=pod
+
+You can use this list of regexes to ignore modules matching
+to be listed as prerequisites of a package. Particulaly useful
+if they are bundled with core-perl anyway and they have known
+issues building.
+
+Toggle it by supplying the C<--default-ignorelist> option.
+
+=cut
+
+sub _default_ignore_list {
+
+ my $list = << '=cut';
+=pod
+
+ ^IO$ # Provided with core anyway
+ ^Cwd$ # Provided with core anyway
+ ^File::Spec # Provided with core anyway
+ ^Config$ # Perl's own config, not shipped separately
+ ^ExtUtils::MakeMaker$ # Shipped with perl, recent versions
+ # have bug 14721 (see rt.cpan.org)
+ ^ExtUtils::Install$ # Part of of EU::MM, same reason
+
+=cut
+
+ return $list;
+}
+
+=head2 Built-In Ban list
+
+You can use this list of regexes to disable building of these
+modules altogether.
+
+Toggle it by supplying the C<--default-banlist> option.
+
+=cut
+
+sub _default_ban_list {
+
+ my $list = << '=cut';
+=pod
+
+ ^GD$ # Needs c libaries
+ ^Berk.*DB # DB packages require specific options & linking
+ ^DBD:: # DBD drives require database files/headers
+ ^XML:: # XML modules usually require expat libraries
+ Apache # These usually require apache libraries
+ SSL # These usually require SSL certificates & libs
+ Image::Magick # Needs ImageMagick C libraries
+ Mail::ClamAV # Needs ClamAV C Libraries
+ ^Verilog # Needs Verilog C Libraries
+ ^Authen::PAM$ # Needs PAM C libraries & Headers
+
+=cut
+
+ return $list;
+}
+
+__END__
+
+=head1 SEE ALSO
+
+L<CPANPLUS::Dist>, L<CPANPLUS::Module>, L<CPANPLUS::Shell::Default>,
+C<cpanp>
+
+=head1 BUG REPORTS
+
+Please report bugs or other issues to E<lt>bug-cpanplus@rt.cpan.org<gt>.
+
+=head1 AUTHOR
+
+This module by Jos Boumans E<lt>kane@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+The CPAN++ interface (of which this module is a part of) is copyright (c)
+2001 - 2007, Jos Boumans E<lt>kane@cpan.orgE<gt>. All rights reserved.
+
+This library is free software; you may redistribute and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/bin/cpanp b/lib/CPANPLUS/bin/cpanp
new file mode 100644
index 0000000000..b1a8f9e016
--- /dev/null
+++ b/lib/CPANPLUS/bin/cpanp
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+# $File: //depot/cpanplus/dist/bin/cpanp $
+# $Revision: #8 $ $Change: 8345 $ $DateTime: 2003/10/05 19:25:48 $
+
+use strict;
+use vars '$VERSION';
+
+use CPANPLUS;
+$VERSION = CPANPLUS->VERSION;
+
+use CPANPLUS::Shell qw[Default];
+my $shell = CPANPLUS::Shell->new;
+
+### if we're given a command, run it; otherwise, open a shell.
+if (@ARGV) {
+ ### take the command line arguments as a command
+ my $input = "@ARGV";
+ ### if they said "--help", fix it up to work.
+ $input = 'h' if $input =~ /^\s*--?h(?:elp)?\s*$/i;
+ ### strip the leading dash
+ $input =~ s/^\s*-//;
+ ### pass the command line to the shell
+ $shell->dispatch_on_input(input => $input, noninteractive => 1);
+} else {
+ ### open a shell for the user
+ $shell->shell();
+}
+
+=head1 NAME
+
+cpanp - The CPANPLUS launcher
+
+=head1 SYNOPSIS
+
+B<cpanp>
+
+B<cpanp> S<[-]B<a>> S<[ --[B<no>-]I<option>... ]> S< I<author>... >
+
+B<cpanp> S<[-]B<mfitulrcz>> S<[ --[B<no>-]I<option>... ]> S< I<module>... >
+
+B<cpanp> S<[-]B<d>> S<[ --[B<no>-]I<option>... ]> S<[ --B<fetchdir>=... ]> S< I<module>... >
+
+B<cpanp> S<[-]B<xb>> S<[ --[B<no>-]I<option>... ]>
+
+B<cpanp> S<[-]B<o>> S<[ --[B<no>-]I<option>... ]> S<[ I<module>... ]>
+
+=head1 DESCRIPTION
+
+This script launches the B<CPANPLUS> utility to perform various operations
+from the command line. If it's invoked without arguments, an interactive
+shell is executed by default.
+
+Optionally, it can take a single-letter switch and one or more argument,
+to perform the associated action on each arguments. A summary of the
+available commands is listed below; C<cpanp -h> provides a detailed list.
+
+ h # help information
+ v # version information
+
+ a AUTHOR ... # search by author(s)
+ m MODULE ... # search by module(s)
+ f MODULE ... # list all releases of a module
+
+ i MODULE ... # install module(s)
+ t MODULE ... # test module(s)
+ u MODULE ... # uninstall module(s)
+ d MODULE ... # download module(s)
+ l MODULE ... # display detailed information about module(s)
+ r MODULE ... # display README files of module(s)
+ c MODULE ... # check for module report(s) from cpan-testers
+ z MODULE ... # extract module(s) and open command prompt in it
+
+ x # reload CPAN indices
+
+ o [ MODULE ... ] # list installed module(s) that aren't up to date
+ b # write a bundle file for your configuration
+
+Each command may be followed by one or more I<options>. If preceded by C<no>,
+the corresponding option will be set to C<0>, otherwise it's set to C<1>.
+
+Example: To skip a module's tests,
+
+ cpanp -i --skiptest MODULE ...
+
+Valid options for most commands are C<cpantest>, C<debug>, C<flush>, C<force>,
+C<prereqs>, C<storable>, C<verbose>, C<md5>, C<signature>, and C<skiptest>; the
+'d' command also accepts C<fetchdir>. Please consult L<CPANPLUS::Configure>
+for an explanation to their meanings.
+
+Example: To download a module's tarball to the current directory,
+
+ cpanp -d --fetchdir=. MODULE ...
+
+=cut
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/bin/cpanp-run-perl b/lib/CPANPLUS/bin/cpanp-run-perl
new file mode 100644
index 0000000000..34e62bddd0
--- /dev/null
+++ b/lib/CPANPLUS/bin/cpanp-run-perl
@@ -0,0 +1,10 @@
+use strict;
+my $old = select STDERR; $|++; # turn on autoflush
+select $old; $|++; # turn on autoflush
+$0 = shift(@ARGV); # rename the script
+my $rv = do($0); # execute the file
+die $@ if $@; # die on parse/execute error
+
+### XXX 'do' returns last statement evaluated, which may be
+### undef as well. So don't die in that case.
+#die $! if not defined $rv; # die on execute error
diff --git a/lib/CPANPLUS/inc.pm b/lib/CPANPLUS/inc.pm
new file mode 100644
index 0000000000..000a0ce92a
--- /dev/null
+++ b/lib/CPANPLUS/inc.pm
@@ -0,0 +1,522 @@
+package CPANPLUS::inc;
+
+=head1 NAME
+
+CPANPLUS::inc
+
+=head1 DESCRIPTION
+
+OBSOLETE
+
+=cut
+
+sub original_perl5opt { $ENV{PERL5OPT} };
+sub original_perl5lib { $ENV{PERL5LIB} };
+sub original_inc { @INC };
+
+1;
+
+__END__
+
+use strict;
+use vars qw[$DEBUG $VERSION $ENABLE_INC_HOOK %LIMIT $QUIET];
+use File::Spec ();
+use Config ();
+
+### 5.6.1. nags about require + bareword otherwise ###
+use lib ();
+
+$QUIET = 0;
+$DEBUG = 0;
+%LIMIT = ();
+
+=pod
+
+=head1 NAME
+
+CPANPLUS::inc - runtime inclusion of privately bundled modules
+
+=head1 SYNOPSIS
+
+ ### set up CPANPLUS::inc to do it's thing ###
+ BEGIN { use CPANPLUS::inc };
+
+ ### enable debugging ###
+ use CPANPLUS::inc qw[DEBUG];
+
+=head1 DESCRIPTION
+
+This module enables the use of the bundled modules in the
+C<CPANPLUS/inc> directory of this package. These modules are bundled
+to make sure C<CPANPLUS> is able to bootstrap itself. It will do the
+following things:
+
+=over 4
+
+=item Put a coderef at the beginning of C<@INC>
+
+This allows us to decide which module to load, and where to find it.
+For details on what we do, see the C<INTERESTING MODULES> section below.
+Also see the C<CAVEATS> section.
+
+=item Add the full path to the C<CPANPLUS/inc> directory to C<$ENV{PERL5LIB>.
+
+This allows us to find our bundled modules even if we spawn off a new
+process. Although it's not able to do the selective loading as the
+coderef in C<@INC> could, it's a good fallback.
+
+=back
+
+=head1 METHODS
+
+=head2 CPANPLUS::inc->inc_path()
+
+Returns the full path to the C<CPANPLUS/inc> directory.
+
+=head2 CPANPLUS::inc->my_path()
+
+Returns the full path to be added to C<@INC> to load
+C<CPANPLUS::inc> from.
+
+=head2 CPANPLUS::inc->installer_path()
+
+Returns the full path to the C<CPANPLUS/inc/installers> directory.
+
+=cut
+
+{ my $ext = '.pm';
+ my $file = (join '/', split '::', __PACKAGE__) . $ext;
+
+ ### os specific file path, if you're not on unix
+ my $osfile = File::Spec->catfile( split('::', __PACKAGE__) ) . $ext;
+
+ ### this returns a unixy path, compensate if you're on non-unix
+ my $path = File::Spec->rel2abs(
+ File::Spec->catfile( split '/', $INC{$file} )
+ );
+
+ ### don't forget to quotemeta; win32 paths are special
+ my $qm_osfile = quotemeta $osfile;
+ my $path_to_me = $path; $path_to_me =~ s/$qm_osfile$//i;
+ my $path_to_inc = $path; $path_to_inc =~ s/$ext$//i;
+ my $path_to_installers = File::Spec->catdir( $path_to_inc, 'installers' );
+
+ sub inc_path { return $path_to_inc }
+ sub my_path { return $path_to_me }
+ sub installer_path { return $path_to_installers }
+}
+
+=head2 CPANPLUS::inc->original_perl5lib
+
+Returns the value of $ENV{PERL5LIB} the way it was when C<CPANPLUS::inc>
+got loaded.
+
+=head2 CPANPLUS::inc->original_perl5opt
+
+Returns the value of $ENV{PERL5OPT} the way it was when C<CPANPLUS::inc>
+got loaded.
+
+=head2 CPANPLUS::inc->original_inc
+
+Returns the value of @INC the way it was when C<CPANPLUS::inc> got
+loaded.
+
+=head2 CPANPLUS::inc->limited_perl5opt(@modules);
+
+Returns a string you can assign to C<$ENV{PERL5OPT}> to have a limited
+include facility from C<CPANPLUS::inc>. It will roughly look like:
+
+ -I/path/to/cpanplus/inc -MCPANPLUS::inc=module1,module2
+
+=cut
+
+{ my $org_opt = $ENV{PERL5OPT};
+ my $org_lib = $ENV{PERL5LIB};
+ my @org_inc = @INC;
+
+ sub original_perl5opt { $org_opt || ''};
+ sub original_perl5lib { $org_lib || ''};
+ sub original_inc { @org_inc, __PACKAGE__->my_path };
+
+ sub limited_perl5opt {
+ my $pkg = shift;
+ my $lim = join ',', @_ or return;
+
+ ### -Icp::inc -Mcp::inc=mod1,mod2,mod3
+ my $opt = '-I' . __PACKAGE__->my_path . ' ' .
+ '-M' . __PACKAGE__ . "=$lim";
+
+ $opt .= $Config::Config{'path_sep'} .
+ CPANPLUS::inc->original_perl5opt
+ if CPANPLUS::inc->original_perl5opt;
+
+ return $opt;
+ }
+}
+
+=head2 CPANPLUS::inc->interesting_modules()
+
+Returns a hashref with modules we're interested in, and the minimum
+version we need to find.
+
+It would looks something like this:
+
+ { File::Fetch => 0.06,
+ IPC::Cmd => 0.22,
+ ....
+ }
+
+=cut
+
+{
+ my $map = {
+ ### used to have 0.80, but not it was never released by coral
+ ### 0.79 *should* be good enough for now... asked coral to
+ ### release 0.80 on 10/3/2006
+ 'IPC::Run' => '0.79',
+ 'File::Fetch' => '0.07',
+ #'File::Spec' => '0.82', # can't, need it ourselves...
+ 'IPC::Cmd' => '0.24',
+ 'Locale::Maketext::Simple' => 0,
+ 'Log::Message' => 0,
+ 'Module::Load' => '0.10',
+ 'Module::Load::Conditional' => '0.07',
+ 'Params::Check' => '0.22',
+ 'Term::UI' => '0.05',
+ 'Archive::Extract' => '0.07',
+ 'Archive::Tar' => '1.23',
+ 'IO::Zlib' => '1.04',
+ 'Object::Accessor' => '0.03',
+ 'Module::CoreList' => '1.97',
+ 'Module::Pluggable' => '2.4',
+ 'Module::Loaded' => 0,
+ #'Config::Auto' => 0, # not yet, not using it yet
+ };
+
+ sub interesting_modules { return $map; }
+}
+
+
+=head1 INTERESTING MODULES
+
+C<CPANPLUS::inc> doesn't even bother to try find and find a module
+it's not interested in. A list of I<interesting modules> can be
+obtained using the C<interesting_modules> method described above.
+
+Note that all subclassed modules of an C<interesting module> will
+also be attempted to be loaded, but a version will not be checked.
+
+When it however does encounter a module it is interested in, it will
+do the following things:
+
+=over 4
+
+=item Loop over your @INC
+
+And for every directory it finds there (skipping all non directories
+-- see the C<CAVEATS> section), see if the module requested can be
+found there.
+
+=item Check the version on every suitable module found in @INC
+
+After a list of modules has been gathered, the version of each of them
+is checked to find the one with the highest version, and return that as
+the module to C<use>.
+
+This enables us to use a recent enough version from our own bundled
+modules, but also to use a I<newer> module found in your path instead,
+if it is present. Thus having access to bugfixed versions as they are
+released.
+
+If for some reason no satisfactory version could be found, a warning
+will be emitted. See the C<DEBUG> section for more details on how to
+find out exactly what C<CPANPLUS::inc> is doing.
+
+=back
+
+=cut
+
+{ my $Loaded;
+ my %Cache;
+
+
+ ### returns the path to a certain module we found
+ sub path_to {
+ my $self = shift;
+ my $mod = shift or return;
+
+ ### find the directory
+ my $path = $Cache{$mod}->[0][2] or return;
+
+ ### probe them explicitly for a special file, because the
+ ### dir we found the file in vs our own paths may point to the
+ ### same location, but might not pass an 'eq' test.
+
+ ### it's our inc-path
+ return __PACKAGE__->inc_path
+ if -e File::Spec->catfile( $path, '.inc' );
+
+ ### it's our installer path
+ return __PACKAGE__->installer_path
+ if -e File::Spec->catfile( $path, '.installers' );
+
+ ### it's just some dir...
+ return $path;
+ }
+
+ ### just a debug method
+ sub _show_cache { return \%Cache };
+
+ sub import {
+ my $pkg = shift;
+
+ ### filter DEBUG, and toggle the global
+ map { $LIMIT{$_} = 1 }
+ grep { /DEBUG/ ? ++$DEBUG && 0 :
+ /QUIET/ ? ++$QUIET && 0 :
+ 1
+ } @_;
+
+ ### only load once ###
+ return 1 if $Loaded++;
+
+ ### first, add our own private dir to the end of @INC:
+ {
+ push @INC, __PACKAGE__->my_path, __PACKAGE__->inc_path,
+ __PACKAGE__->installer_path;
+
+ ### XXX stop doing this, there's no need for it anymore;
+ ### none of the shell outs need to have this set anymore
+# ### add the path to this module to PERL5OPT in case
+# ### we spawn off some programs...
+# ### then add this module to be loaded in PERL5OPT...
+# { local $^W;
+# $ENV{'PERL5LIB'} .= $Config::Config{'path_sep'}
+# . __PACKAGE__->my_path
+# . $Config::Config{'path_sep'}
+# . __PACKAGE__->inc_path;
+#
+# $ENV{'PERL5OPT'} = '-M'. __PACKAGE__ . ' '
+# . ($ENV{'PERL5OPT'} || '');
+# }
+ }
+
+ ### next, find the highest version of a module that
+ ### we care about. very basic check, but will
+ ### have to do for now.
+ lib->import( sub {
+ my $path = pop(); # path to the pm
+ my $module = $path or return; # copy of the path, to munge
+ my @parts = split qr!\\|/!, $path; # dirs + file name; could be
+ # win32 paths =/
+ my $file = pop @parts; # just the file name
+ my $map = __PACKAGE__->interesting_modules;
+
+ ### translate file name to module name
+ ### could contain win32 paths delimiters
+ $module =~ s!/|\\!::!g; $module =~ s/\.pm//i;
+
+ my $check_version; my $try;
+ ### does it look like a module we care about?
+ my ($interesting) = grep { $module =~ /^$_/ } keys %$map;
+ ++$try if $interesting;
+
+ ### do we need to check the version too?
+ ++$check_version if exists $map->{$module};
+
+ ### we don't care ###
+ unless( $try ) {
+ warn __PACKAGE__ .": Not interested in '$module'\n" if $DEBUG;
+ return;
+
+ ### we're not allowed
+ } elsif ( $try and keys %LIMIT ) {
+ unless( grep { $module =~ /^$_/ } keys %LIMIT ) {
+ warn __PACKAGE__ .": Limits active, '$module' not allowed ".
+ "to be loaded" if $DEBUG;
+ return;
+ }
+ }
+
+ ### found filehandles + versions ###
+ my @found;
+ DIR: for my $dir (@INC) {
+ next DIR unless -d $dir;
+
+ ### get the full path to the module ###
+ my $pm = File::Spec->catfile( $dir, @parts, $file );
+
+ ### open the file if it exists ###
+ if( -e $pm ) {
+ my $fh;
+ unless( open $fh, "$pm" ) {
+ warn __PACKAGE__ .": Could not open '$pm': $!\n"
+ if $DEBUG;
+ next DIR;
+ }
+
+ my $found;
+ ### XXX stolen from module::load::conditional ###
+ while (local $_ = <$fh> ) {
+
+ ### the following regexp comes from the
+ ### ExtUtils::MakeMaker documentation.
+ if ( /([\$*])(([\w\:\']*)\bVERSION)\b.*\=/ ) {
+
+ ### this will eval the version in to $VERSION if it
+ ### was declared as $VERSION in the module.
+ ### else the result will be in $res.
+ ### this is a fix on skud's Module::InstalledVersion
+
+ local $VERSION;
+ my $res = eval $_;
+
+ ### default to '0.0' if there REALLY is no version
+ ### all to satisfy warnings
+ $found = $VERSION || $res || '0.0';
+
+ ### found what we came for
+ last if $found;
+ }
+ }
+
+ ### no version defined at all? ###
+ $found ||= '0.0';
+
+ warn __PACKAGE__ .": Found match for '$module' in '$dir' "
+ ."with version '$found'\n" if $DEBUG;
+
+ ### reset the position of the filehandle ###
+ seek $fh, 0, 0;
+
+ ### store the found version + filehandle it came from ###
+ push @found, [ $found, $fh, $dir, $pm ];
+ }
+
+ } # done looping over all the dirs
+
+ ### nothing found? ###
+ unless (@found) {
+ warn __PACKAGE__ .": Unable to find any module named "
+ . "'$module'\n" if $DEBUG;
+ return;
+ }
+
+ ### find highest version
+ ### or the one in the same dir as a base module already loaded
+ ### or otherwise, the one not bundled
+ ### or otherwise the newest
+ my @sorted = sort {
+ _vcmp($b->[0], $a->[0]) ||
+ ($Cache{$interesting}
+ ?($b->[2] eq $Cache{$interesting}->[0][2]) <=>
+ ($a->[2] eq $Cache{$interesting}->[0][2])
+ : 0 ) ||
+ (($a->[2] eq __PACKAGE__->inc_path) <=>
+ ($b->[2] eq __PACKAGE__->inc_path)) ||
+ (-M $a->[3] <=> -M $b->[3])
+ } @found;
+
+ warn __PACKAGE__ .": Best match for '$module' is found in "
+ ."'$sorted[0][2]' with version '$sorted[0][0]'\n"
+ if $DEBUG;
+
+ if( $check_version and
+ not (_vcmp($sorted[0][0], $map->{$module}) >= 0)
+ ) {
+ warn __PACKAGE__ .": Cannot find high enough version for "
+ ."'$module' -- need '$map->{$module}' but "
+ ."only found '$sorted[0][0]'. Returning "
+ ."highest found version but this may cause "
+ ."problems\n" unless $QUIET;
+ };
+
+ ### right, so that damn )#$(*@#)(*@#@ Module::Build makes
+ ### assumptions about the environment (especially its own tests)
+ ### and blows up badly if it's loaded via CP::inc :(
+ ### so, if we find a newer version on disk (which would happen when
+ ### upgrading or having upgraded, just pretend we didn't find it,
+ ### let it be loaded via the 'normal' way.
+ ### can't even load the *proper* one via our CP::inc, as it will
+ ### get upset just over the fact it's loaded via a non-standard way
+ if( $module =~ /^Module::Build/ and
+ $sorted[0][2] ne __PACKAGE__->inc_path and
+ $sorted[0][2] ne __PACKAGE__->installer_path
+ ) {
+ warn __PACKAGE__ .": Found newer version of 'Module::Build::*' "
+ ."elsewhere in your path. Pretending to not "
+ ."have found it\n" if $DEBUG;
+ return;
+ }
+
+ ### store what we found for this module
+ $Cache{$module} = \@sorted;
+
+ ### best matching filehandle ###
+ return $sorted[0][1];
+ } );
+ }
+}
+
+### XXX copied from C::I::Utils, so there's no circular require here!
+sub _vcmp {
+ my ($x, $y) = @_;
+ s/_//g foreach $x, $y;
+ return $x <=> $y;
+}
+
+=pod
+
+=head1 DEBUG
+
+Since this module does C<Clever Things> to your search path, it might
+be nice sometimes to figure out what it's doing, if things don't work
+as expected. You can enable a debug trace by calling the module like
+this:
+
+ use CPANPLUS::inc 'DEBUG';
+
+This will show you what C<CPANPLUS::inc> is doing, which might look
+something like this:
+
+ CPANPLUS::inc: Found match for 'Params::Check' in
+ '/opt/lib/perl5/site_perl/5.8.3' with version '0.07'
+ CPANPLUS::inc: Found match for 'Params::Check' in
+ '/my/private/lib/CPANPLUS/inc' with version '0.21'
+ CPANPLUS::inc: Best match for 'Params::Check' is found in
+ '/my/private/lib/CPANPLUS/inc' with version '0.21'
+
+=head1 CAVEATS
+
+This module has 2 major caveats, that could lead to unexpected
+behaviour. But currently I don't know how to fix them, Suggestions
+are much welcomed.
+
+=over 4
+
+=item On multiple C<use lib> calls, our coderef may not be the first in @INC
+
+If this happens, although unlikely in most situations and not happening
+when calling the shell directly, this could mean that a lower (too low)
+versioned module is loaded, which might cause failures in the
+application.
+
+=item Non-directories in @INC
+
+Non-directories are right now skipped by CPANPLUS::inc. They could of
+course lead us to newer versions of a module, but it's too tricky to
+verify if they would. Therefor they are skipped. In the worst case
+scenario we'll find the sufficing version bundled with CPANPLUS.
+
+
+=cut
+
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/t/00_CPANPLUS-Inc.t b/lib/CPANPLUS/t/00_CPANPLUS-Inc.t
new file mode 100644
index 0000000000..cf78d61f59
--- /dev/null
+++ b/lib/CPANPLUS/t/00_CPANPLUS-Inc.t
@@ -0,0 +1,190 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use Test::More 'no_plan';
+
+my $Class = 'CPANPLUS::inc';
+use_ok( $Class );
+can_ok( $Class, qw[original_perl5opt original_perl5lib original_inc] );
+
+__END__
+
+# XXX CPANPLUS::inc functionality is obsolete, so it is removed
+
+my $Module = 'Params::Check';
+my $File = File::Spec->catfile(qw|Params Check.pm|);
+my $Ufile = 'Params/Check.pm';
+my $Boring = 'IO::File';
+my $Bfile = 'IO/File.pm';
+
+
+
+### now, first element should be a coderef ###
+my $code = $INC[0];
+is( ref $code, 'CODE', 'Coderef loaded in @INC' );
+
+### check interesting modules ###
+{ my $mods = CPANPLUS::inc->interesting_modules();
+ ok( $mods, "Retrieved interesting modules list" );
+ is( ref $mods, 'HASH', " It's a hashref" );
+ ok( scalar(keys %$mods), " With some keys in it" );
+ ok( $mods->{$Module}, " Found a module we care about" );
+}
+
+### checking include path ###
+SKIP: {
+ my $path = CPANPLUS::inc->inc_path();
+ ok( $path, "Retrieved include path" );
+ ok( -d $path, " Include path is an actual directory" );
+
+ ### XXX no more files are bundled this way, it's obsolete
+ #skip "No files actually bundled in perl core", 1 if $ENV{PERL_CORE};
+ #ok( -s File::Spec->catfile( $path, $File ),
+ # " Found '$File' in include path" );
+
+ ### we don't do this anymore
+ #my $out = join '', `$^X -V`; my $qm_path = quotemeta $path;
+ #like( $out, qr/$qm_path/s, " Path found in perl -V output" );
+}
+
+### back to the coderef ###
+### try a boring module ###
+{ local $CPANPLUS::inc::DEBUG = 1;
+ my $warnings; local $SIG{__WARN__} = sub { $warnings .= "@_" };
+
+ my $rv = $code->($code, $Bfile);
+ ok( !$rv, "Ignoring boring module" );
+ ok( !$INC{$Bfile}, " Boring file not loaded" );
+ like( $warnings, qr/CPANPLUS::inc: Not interested in '$Boring'/s,
+ " Warned about boringness" );
+}
+
+### try to load a module with windows paths in it (bug [#11177])
+{ local $CPANPLUS::inc::DEBUG = 1;
+ my $warnings; local $SIG{__WARN__} = sub { $warnings .= "@_" };
+
+ my $wfile = 'IO\File.pm';
+ my $wmod = 'IO::File';
+
+ my $rv = $code->($code, $wfile);
+ ok( !$rv, "Ignoring boring win32 module" );
+ ok( !$INC{$wfile}, " Boring win32 file not loaded" );
+ like( $warnings, qr/CPANPLUS::inc: Not interested in '$wmod'/s,
+ " Warned about boringness" );
+}
+
+### try an interesting module ###
+{ local $CPANPLUS::inc::DEBUG = 1;
+ my $warnings; local $SIG{__WARN__} = sub { $warnings .= "@_" };
+
+ my $rv = $code->($code, $Ufile);
+ ok( $rv, "Found interesting module" );
+ ok( !$INC{$Ufile}, " Interesting file not loaded" );
+ like( $warnings, qr/CPANPLUS::inc: Found match for '$Module'/,
+ " Match noted in warnings" );
+ like( $warnings, qr/CPANPLUS::inc: Best match for '$Module'/,
+ " Best match noted in warnings" );
+
+ my $contents = do { local $/; <$rv> };
+ ok( $contents, " Read contents from filehandle" );
+ like( $contents, qr/$Module/s,
+ " Contents is from '$Module'" );
+}
+
+### now do some real loading ###
+{ use_ok($Module);
+ ok( $INC{$Ufile}, " Regular use of '$Module'" );
+
+ use_ok($Boring);
+ ok( $INC{$Bfile}, " Regular use of '$Boring'" );
+}
+
+### check we didn't load our coderef anymore than needed ###
+{ my $amount = 5;
+ for( 0..$amount ) { CPANPLUS::inc->import; };
+
+ my $flag;
+ map { $flag++ if ref $_ eq 'CODE' } @INC[0..$amount];
+
+ my $ok = $amount + 1 == $flag ? 0 : 1;
+ ok( $ok, "Only loaded coderef into \@INC $flag times");
+}
+
+### check limit funcionality
+{ local $CPANPLUS::inc::DEBUG = 1;
+ my $warnings; local $SIG{__WARN__} = sub { $warnings .= "@_" };
+
+ ### so we can reload it
+ delete $INC{$Ufile};
+ delete $INC{$Bfile};
+
+ ### limit to the loading of $Boring;
+ CPANPLUS::inc->import( $Boring );
+
+ ok( $CPANPLUS::inc::LIMIT{$Boring},
+ "Limit to '$Boring' recorded" );
+
+ ### try a boring file first
+ { my $rv = $code->($code, $Bfile);
+ ok( !$rv, " '$Boring' still not being loaded" );
+ ok( !$INC{$Bfile}, ' Is not in %INC either' );
+ }
+
+ ### try an interesting one now
+ { my $rv = $code->( $code, $Ufile );
+ ok( !$rv, " '$Module' is not being loaded" );
+ ok( !$INC{$Ufile}, ' Is not in %INC either' );
+ like( $warnings, qr/CPANPLUS::inc: Limits active, '$Module'/s,
+ " Warned about limits" );
+ }
+
+ ### reset limits, try again
+ { local %CPANPLUS::inc::LIMIT;
+ ok( !keys(%CPANPLUS::inc::LIMIT),
+ " Limits removed" );
+
+
+ my $rv = $code->( $code, $Ufile );
+ ok( $rv, " '$Module' is being loaded" );
+
+ use_ok( $Module );
+ ok( $INC{$Ufile}, ' Present in %INC' );
+ }
+}
+
+### test limited perl5opt, to include just a few modules
+{ my $dash_m = quotemeta '-MCPANPLUS::inc';
+ my $dash_i = quotemeta '-I' . CPANPLUS::inc->my_path;
+ my $orgopt = quotemeta CPANPLUS::inc->original_perl5opt;
+
+ ### first try an empty string;
+ { my $str = CPANPLUS::inc->limited_perl5opt;
+ ok( !$str, "limited_perl5opt without args is empty" );
+ }
+
+ ### try with one 'modules'
+ { my $str = CPANPLUS::inc->limited_perl5opt(qw[A]);
+ ok( $str, "limted perl5opt set for 1 module" );
+ like( $str, qr/$dash_m=A\b/,
+ " -M set properly" );
+ like( $str, qr/$dash_i/," -I set properly" );
+ like( $str, qr/$orgopt/," Original opts preserved" );
+ }
+
+ ### try with more 'modules'
+ { my $str = CPANPLUS::inc->limited_perl5opt(qw[A B C]);
+ ok( $str, "limted perl5opt set for 3 modules" );
+ like( $str, qr/$dash_m=A,B,C\b/,
+ " -M set properly" );
+ like( $str, qr/$dash_i/," -I set properly" );
+ like( $str, qr/$orgopt/," Original opts preserved" );
+ }
+}
+
+
+
+
diff --git a/lib/CPANPLUS/t/00_CPANPLUS-Internals-Utils.t b/lib/CPANPLUS/t/00_CPANPLUS-Internals-Utils.t
new file mode 100644
index 0000000000..83a4095995
--- /dev/null
+++ b/lib/CPANPLUS/t/00_CPANPLUS-Internals-Utils.t
@@ -0,0 +1,132 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+
+### make sure to keep the plan -- this is the only test
+### supported for 'older' T::H (pre 2.28) -- see Makefile.PL for details
+use Test::More tests => 36;
+
+use Cwd;
+use Data::Dumper;
+use File::Spec;
+use File::Basename;
+
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Utils;
+
+my $Cwd = File::Spec->rel2abs(cwd());
+my $Class = 'CPANPLUS::Internals::Utils';
+my $Dir = 'foo';
+my $Move = 'bar';
+my $File = 'zot';
+
+rmdir $Move if -d $Move;
+rmdir $Dir if -d $Dir;
+
+### test _mdkir ###
+{ ok( $Class->_mkdir( dir => $Dir), "Created dir '$Dir'" );
+ ok( -d $Dir, " '$Dir' is a dir" );
+}
+
+### test _chdir ###
+{ ok( $Class->_chdir( dir => $Dir), "Chdir to '$Dir'" );
+ is( File::Spec->rel2abs(cwd()), File::Spec->rel2abs(File::Spec->catdir($Cwd,$Dir)),
+ " Cwd() is '$Dir'");
+ ok( $Class->_chdir( dir => $Cwd), "Chdir back to '$Cwd'" );
+ is( File::Spec->rel2abs(cwd()),$Cwd," Cwd() is '$Cwd'" );
+}
+
+### test _move ###
+{ ok( $Class->_move( file => $Dir, to => $Move ),
+ "Move from '$Dir' to '$Move'" );
+ ok( -d $Move, " Dir '$Move' exists" );
+ ok( !-d $Dir, " Dir '$Dir' no longer exists" );
+
+
+ { local $CPANPLUS::Error::ERROR_FH = output_handle();
+
+ ### now try to move it somewhere it can't ###
+ ok( !$Class->_move( file => $Move, to => 'inc' ),
+ " Impossible move detected" );
+ like( CPANPLUS::Error->stack_as_string, qr/Failed to move/,
+ " Expected error found" );
+ }
+}
+
+### test _rmdir ###
+{ ok( -d $Move, "Dir '$Move' exists" );
+ ok( $Class->_rmdir( dir => $Move ), " Deleted dir '$Move'" );
+ ok(!-d $Move, " Dir '$Move' no longer exists" );
+}
+
+### _get_file_contents tests ###
+{ my $contents = $Class->_get_file_contents( file => basename($0) );
+ ok( $contents, "Got file contents" );
+ like( $contents, qr/BEGIN/, " Proper contents found" );
+ like( $contents, qr/CPANPLUS/, " Proper contents found" );
+}
+
+### _perl_version tests ###
+{ my $version = $Class->_perl_version( perl => $^X );
+ ok( $version, "Perl version found" );
+ like( $version, qr/\d.\d.\d/, " Looks like a proper version" );
+}
+
+### _version_to_number tests ###
+{ my $map = {
+ '1' => '1',
+ '1.2' => '1.2',
+ '.2' => '.2',
+ 'foo' => '0.0',
+ 'a.1' => '0.0',
+ };
+
+ while( my($try,$expect) = each %$map ) {
+ my $ver = $Class->_version_to_number( version => $try );
+ ok( $ver, "Version returned" );
+ is( $ver, $expect, " Value as expected" );
+ }
+}
+
+### _whoami tests ###
+{ sub foo {
+ my $me = $Class->_whoami;
+ ok( $me, "_whoami returned a result" );
+ is( $me, 'foo', " Value as expected" );
+ }
+
+ foo();
+}
+
+### _mode_plus_w tests ###
+{ open my $fh, ">$File" or die "Could not open $File for writing: $!";
+ close $fh;
+
+ ### remove perms
+ ok( -e $File, "File '$File' created" );
+ ok( chmod( 000, $File ), " File permissions set to 000" );
+
+ ok( $Class->_mode_plus_w( file => $File ),
+ " File permissions set to +w" );
+ ok( -w $File, " File is writable" );
+
+ 1 while unlink $File;
+
+ ok( !-e $File, " File removed" );
+}
+
+
+
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/t/01_CPANPLUS-Configure.t b/lib/CPANPLUS/t/01_CPANPLUS-Configure.t
new file mode 100644
index 0000000000..274e04f198
--- /dev/null
+++ b/lib/CPANPLUS/t/01_CPANPLUS-Configure.t
@@ -0,0 +1,142 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use Test::More 'no_plan';
+use Data::Dumper;
+use strict;
+use CPANPLUS::Internals::Constants;
+
+### purposely avert messages and errors to a file? ###
+my $Trap_Output = @ARGV ? 0 : 1;
+my $Config_pm = 'CPANPLUS/Config.pm';
+
+### DO NOT FLUSH TILL THE END!!! we depend on all warnings being logged..
+
+for my $mod (qw[CPANPLUS::Configure]) {
+ use_ok($mod) or diag qq[Can't load $mod];
+}
+
+my $c = CPANPLUS::Configure->new();
+isa_ok($c, 'CPANPLUS::Configure');
+
+my $r = $c->conf;
+isa_ok( $r, 'CPANPLUS::Config' );
+
+
+### EU::AI compatibility test ###
+{ my $base = $c->_get_build('base');
+ ok( defined($base), "Base retrieved by old compat API");
+ is( $base, $c->get_conf('base'), " Value as expected" );
+}
+
+for my $cat ( $r->ls_accessors ) {
+
+ ### what field can they take? ###
+ my @options = $c->options( type => $cat );
+
+ ### copy for use on the config object itself
+ my $accessor = $cat;
+ my $prepend = ($cat =~ s/^_//) ? '_' : '';
+
+ my $getmeth = $prepend . 'get_'. $cat;
+ my $setmeth = $prepend . 'set_'. $cat;
+ my $addmeth = $prepend . 'add_'. $cat;
+
+ ok( scalar(@options), "Possible options obtained" );
+
+ ### test adding keys too ###
+ { my $add_key = 'test_key';
+ my $add_val = [1..3];
+
+ my $found = grep { $add_key eq $_ } @options;
+ ok( !$found, "Key '$add_key' not yet defined" );
+ ok( $c->$addmeth( $add_key => $add_val ),
+ " $addmeth('$add_key' => VAL)" );
+
+ ### this one now also exists ###
+ push @options, $add_key
+ }
+
+ ### poke in the object, get the actual hashref out ###
+ my %hash = map {
+ $_ => $r->$accessor->$_
+ } $r->$accessor->ls_accessors;
+
+ while( my ($key,$val) = each %hash ) {
+ my $is = $c->$getmeth($key);
+ is_deeply( $val, $is, "deep check for '$key'" );
+ ok( $c->$setmeth($key => 1 ), " $setmeth('$key' => 1)" );
+ is( $c->$getmeth($key), 1, " $getmeth('$key')" );
+ ok( $c->$setmeth($key => $val), " $setmeth('$key' => ORGVAL)" );
+ }
+
+ ### now check if we found all the keys with options or not ###
+ delete $hash{$_} for @options;
+ ok( !(scalar keys %hash), "All possible keys found" );
+
+}
+
+
+### see if we can save the config ###
+{ my $dir = File::Spec->rel2abs('dummy-cpanplus');
+ my $pm = 'CPANPLUS::Config::Test' . $$;
+ my $file = $c->save( $pm, $dir );
+
+ ok( $file, "Config $pm saved" );
+ ok( -e $file, " File exists" );
+ ok( -s $file, " File has size" );
+
+ ### include our dummy dir when re-scanning
+ { local @INC = ( $dir, @INC );
+ ok( $c->init( rescan => 1 ),
+ "Reran ->init()" );
+ }
+
+ ### make sure this file is now loaded
+ ### XXX can't trust bloody dir seperators on Win32 in %INC,
+ ### so rather than an exact match, do a grep...
+ my ($found) = grep /\bTest$$/, values %INC;
+ ok( $found, " Found $file in \%INC" );
+ ok( -e $file, " File exists" );
+ 1 while unlink $file;
+ ok(!-e $file, " File removed" );
+
+}
+
+{ local $CPANPLUS::Error::ERROR_FH = output_handle() if $Trap_Output;
+
+ my $env = ENV_CPANPLUS_CONFIG;
+ local $ENV{$env} = $$;
+ my $ok = $c->init;
+ my $stack = CPANPLUS::Error->stack_as_string;
+
+ ok( $ok, "Reran init again" );
+ like( $stack, qr/Specifying a config file in your environment/,
+ " Warning logged" );
+}
+
+
+{ local $CPANPLUS::Error::ERROR_FH = output_handle() if $Trap_Output;
+
+ CPANPLUS::Error->flush;
+
+ { ### try a bogus method call
+ my $x = $c->flubber('foo');
+ my $err = CPANPLUS::Error->stack_as_string;
+ is ($x, undef, "Bogus method call returns undef");
+ like($err, "/flubber/", " Bogus method call recognized");
+ }
+
+ CPANPLUS::Error->flush;
+}
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/02_CPANPLUS-Internals.t b/lib/CPANPLUS/t/02_CPANPLUS-Internals.t
new file mode 100644
index 0000000000..a9e85838bf
--- /dev/null
+++ b/lib/CPANPLUS/t/02_CPANPLUS-Internals.t
@@ -0,0 +1,123 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use Test::More 'no_plan';
+
+use CPANPLUS::Configure;
+use CPANPLUS::Backend;
+use CPANPLUS::Internals::Constants;
+use Module::Load::Conditional qw[can_load];
+use Data::Dumper;
+
+my $cb = CPANPLUS::Backend->new( CPANPLUS::Configure->new() );
+
+isa_ok($cb, 'CPANPLUS::Internals');
+is($cb->_id, $cb->_last_id, "Comparing ID's");
+
+### delete/store/retrieve id tests ###
+{ my $del = $cb->_remove_id( $cb->_id );
+ ok( $del, "ID deleted" );
+ isa_ok( $del, "CPANPLUS::Internals" );
+ is( $del, $cb, " Deleted ID matches last object" );
+
+ my $id = $cb->_store_id( $del );
+ ok( $id, "ID stored" );
+ is( $id, $cb->_id, " Stored proper ID" );
+
+ my $obj = $cb->_retrieve_id( $id );
+ ok( $obj, "Object retrieved from ID" );
+ isa_ok( $obj, 'CPANPLUS::Internals' );
+ is( $obj->_id, $id, " Retrieved ID properly" );
+
+ my @obs = $cb->_return_all_objects();
+ ok( scalar(@obs), "Returned objects" );
+ is( scalar(@obs), 1, " Proper amount of objects found" );
+ is( $obs[0]->_id, $id, " Proper ID found on object" );
+
+ my $lid = $cb->_last_id;
+ ok( $lid, "Found last registered ID" );
+ is( $lid, $id, " ID matches last object" );
+
+ my $iid = $cb->_inc_id;
+ ok( $iid, "Incremented ID" );
+ is( $iid, $id+1, " ID matched last ID + 1" );
+}
+
+### host ok test ###
+{
+ my $host = $cb->configure_object->get_conf('hosts')->[0];
+
+ is( $cb->_host_ok( host => $host ), 1, "Host ok" );
+ is( $cb->_add_fail_host(host => $host), 1, " Host now marked as bad" );
+ is( $cb->_host_ok( host => $host ), 0, " Host still bad" );
+ ok( $cb->_flush( list => ['hosts'] ), " Hosts flushed" );
+ is( $cb->_host_ok( host => $host ), 1, " Host now ok again" );
+}
+
+### flush loads test
+{ my $mod = 'Benchmark';
+ my $file = $mod . '.pm';
+
+ ### XXX whitebox test -- mark this module as unloadable
+ $Module::Load::Conditional::CACHE->{$mod}->{usable} = 0;
+
+ ok( !can_load( modules => { $mod => 0 }, verbose => 0 ),
+ "'$mod' not loaded" );
+
+ ok( $cb->flush('load'), " 'load' cache flushed" );
+ ok( can_load( modules => { $mod => 0 }, verbose => 0 ),
+ " '$mod' loaded" );
+}
+
+### callback registering tests ###
+{ my $callback_map = {
+ ### name default value
+ install_prerequisite => 1, # install prereqs when 'ask' is set?
+ edit_test_report => 0, # edit the prepared test report?
+ send_test_report => 1, # send the test report?
+ munge_test_report => $$, # munge the test report
+ filter_prereqs => $$, # limit prereqs
+ };
+
+ for my $callback ( keys %$callback_map ) {
+
+ { local $CPANPLUS::Error::ERROR_FH = output_handle() unless @ARGV;
+
+ my $rv = $callback_map->{$callback};
+
+ is( $rv, $cb->_callbacks->$callback->( $0, $$ ),
+ "Default callback '$callback' called" );
+ like( CPANPLUS::Error->stack_as_string, qr/DEFAULT '\S+' HANDLER/s,
+ " Default handler warning recorded" );
+ CPANPLUS::Error->flush;
+ }
+
+ ### try to register the callback
+ my $ok = $cb->_register_callback(
+ name => $callback,
+ code => sub { return $callback }
+ );
+
+ ok( $ok, "Registered callback '$callback' ok" );
+
+ my $sub = $cb->_callbacks->$callback;
+ ok( $sub, " Retrieved callback" );
+ ok( IS_CODEREF->($sub), " Callback is a sub" );
+
+ my $rv = $sub->();
+ ok( $rv, " Callback called ok" );
+ is( $rv, $callback, " Got expected return value" );
+ }
+}
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/03_CPANPLUS-Internals-Source.t b/lib/CPANPLUS/t/03_CPANPLUS-Internals-Source.t
new file mode 100644
index 0000000000..b1d5c04016
--- /dev/null
+++ b/lib/CPANPLUS/t/03_CPANPLUS-Internals-Source.t
@@ -0,0 +1,45 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+
+use CPANPLUS::Backend;
+
+use Test::More 'no_plan';
+use Data::Dumper;
+
+my $conf = gimme_conf();
+
+my $cb = CPANPLUS::Backend->new( $conf );
+isa_ok($cb, "CPANPLUS::Internals" );
+
+my $mt = $cb->_module_tree;
+my $at = $cb->_author_tree;
+my $modname = TEST_CONF_MODULE;
+
+for my $name (qw[auth mod dslip] ) {
+ my $file = File::Spec->catfile(
+ $conf->get_conf('base'),
+ $conf->_get_source($name)
+ );
+ ok( (-e $file && -f _ && -s _), "$file exists" );
+}
+
+ok( scalar keys %$at, "Authortree loaded successfully" );
+ok( scalar keys %$mt, "Moduletree loaded successfully" );
+
+my $auth = $at->{'EUNOXS'};
+my $mod = $mt->{$modname};
+
+isa_ok( $auth, 'CPANPLUS::Module::Author' );
+isa_ok( $mod, 'CPANPLUS::Module' );
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/04_CPANPLUS-Module.t b/lib/CPANPLUS/t/04_CPANPLUS-Module.t
new file mode 100644
index 0000000000..81874ddc80
--- /dev/null
+++ b/lib/CPANPLUS/t/04_CPANPLUS-Module.t
@@ -0,0 +1,302 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+
+use CPANPLUS::Configure;
+use CPANPLUS::Backend;
+use CPANPLUS::Module::Fake;
+use CPANPLUS::Module::Author::Fake;
+use CPANPLUS::Internals::Constants;
+
+use Test::More 'no_plan';
+use Data::Dumper;
+use File::Path ();
+
+### silence errors, unless you tell us not to ###
+local $CPANPLUS::Error::ERROR_FH = output_handle() unless @ARGV;
+
+my $Conf = gimme_conf();
+my $CB = CPANPLUS::Backend->new( $Conf );
+
+### start with fresh sources ###
+ok( $CB->reload_indices( update_source => 0 ), "Rebuilding trees" );
+
+my $AuthName = 'EUNOXS';
+my $Auth = $CB->author_tree( $AuthName );
+my $ModName = TEST_CONF_MODULE;
+my $Mod = $CB->module_tree( $ModName );
+my $CoreName = TEST_CONF_PREREQ;
+my $CoreMod = $CB->module_tree( $CoreName );
+
+isa_ok( $Auth, 'CPANPLUS::Module::Author' );
+isa_ok( $Mod, 'CPANPLUS::Module' );
+isa_ok( $CoreMod, 'CPANPLUS::Module' );
+
+### author accessors ###
+is( $Auth->author, 'ExtUtils::MakeMaker No XS Code',
+ "Author name: " . $Auth->author );
+is( $Auth->cpanid, $AuthName, "Author CPANID: " . $Auth->cpanid );
+is( $Auth->email, DEFAULT_EMAIL,"Author email: " . $Auth->email );
+isa_ok( $Auth->parent, 'CPANPLUS::Backend' );
+
+### module accessors ###
+{ my %map = (
+ ### method ### result
+ module => $ModName,
+ name => $ModName,
+ comment => undef,
+ package => 'Foo-Bar-0.01.tar.gz',
+ path => 'authors/id/E/EU/EUNOXS',
+ version => '0.01',
+ dslip => 'cdpO ',
+ description => 'CPANPLUS Test Package',
+ mtime => '',
+ author => $Auth,
+ );
+
+ my @acc = $Mod->accessors;
+ ok( scalar(@acc), "Retrieved module accessors" );
+
+ ### remove private accessors
+ is_deeply( [ sort keys %map ], [ sort grep { $_ !~ /^_/ } @acc ],
+ " About to test all accessors" );
+
+ ### check all the accessors
+ while( my($meth,$res) = each %map ) {
+ is( $Mod->$meth, $res, " Mod->$meth: " . ($res || '<empty>') );
+ }
+
+ ### check accessor objects ###
+ isa_ok( $Mod->parent, 'CPANPLUS::Backend' );
+ isa_ok( $Mod->author, 'CPANPLUS::Module::Author' );
+ is( $Mod->author->author, $Auth->author,
+ "Module eq Author" );
+}
+
+### convenience methods ###
+{ ok( 1, "Convenience functions" );
+ is( $Mod->package_name, 'Foo-Bar', " Package name");
+ is( $Mod->package_version, '0.01', " Package version");
+ is( $Mod->package_extension, 'tar.gz', " Package extension");
+ ok( !$Mod->package_is_perl_core, " Package not core");
+ ok( !$Mod->module_is_supplied_with_perl_core, " Module not core" );
+ ok( !$Mod->is_bundle, " Package not bundle");
+}
+
+### clone & status tests
+{ my $clone = $Mod->clone;
+ ok( $clone, "Module cloned" );
+ isa_ok( $clone, 'CPANPLUS::Module' );
+
+ for my $acc ( $Mod->accessors ) {
+ is( $clone->$acc, $Mod->$acc,
+ " Clone->$acc matches Mod->$acc " );
+ }
+
+ ### XXX whitebox test
+ ok( !$clone->_status, "Status object empty on start" );
+
+ my $status = $clone->status;
+ ok( $status, " Status object defined after query" );
+ is( $status, $clone->_status,
+ " Object stored as expected" );
+ isa_ok( $status, 'Object::Accessor' );
+}
+
+{ ### extract + error test ###
+ ok( !$Mod->extract(), "Cannot extract unfetched file" );
+ like( CPANPLUS::Error->stack_as_string, qr/You have not fetched/,
+ " Error properly logged" );
+}
+
+{ ### fetch tests ###
+ ### enable signature checks for checksums ###
+ my $old = $Conf->get_conf('signature');
+ $Conf->set_conf(signature => 1);
+
+ my $where = $Mod->fetch( force => 1 );
+ ok( $where, "Module fetched" );
+ ok( -f $where, " Module is a file" );
+ ok( -s $where, " Module has size" );
+
+ $Conf->set_conf( signature => $old );
+}
+
+{ ### extract tests ###
+ my $dir = $Mod->extract( force => 1 );
+ ok( $dir, "Module extracted" );
+ ok( -d $dir, " Dir exsits" );
+}
+
+
+{ ### readme tests ###
+ my $readme = $Mod->readme;
+ ok( length $readme, "Readme found" );
+ is( $readme, $Mod->status->readme,
+ " Readme stored in module object" );
+}
+
+{ ### checksums tests ###
+ SKIP: {
+ skip(q[You chose not to enable checksum verification], 5)
+ unless $Conf->get_conf('md5');
+
+ my $cksum_file = $Mod->checksums( force => 1 );
+ ok( $cksum_file, "Checksum file found" );
+ is( $cksum_file, $Mod->status->checksums,
+ " File stored in module object" );
+ ok( -e $cksum_file, " File exists" );
+ ok( -s $cksum_file, " File has size" );
+
+ ### XXX test checksum_value if there's digest::md5 + config wants it
+ ok( $Mod->status->checksum_ok,
+ " Checksum is ok" );
+ }
+}
+
+
+{ ### installer type tests ###
+ my $installer = $Mod->get_installer_type;
+ ok( $installer, "Installer found" );
+ is( $installer, INSTALLER_MM,
+ " Proper installer found" );
+}
+
+{ ### check signature tests ###
+ SKIP: {
+ skip(q[You chose not to enable signature checks], 1)
+ unless $Conf->get_conf('signature');
+
+ ok( $Mod->check_signature,
+ "Signature check OK" );
+ }
+}
+
+{ ### details() test ###
+ my $href = {
+ 'Support Level' => 'Developer',
+ 'Package' => $Mod->package,
+ 'Description' => $Mod->description,
+ 'Development Stage' =>
+ 'under construction but pre-alpha (not yet released)',
+ 'Author' => sprintf("%s (%s)", $Auth->author, $Auth->email),
+ 'Version on CPAN' => $Mod->version,
+ 'Language Used' =>
+ 'Perl-only, no compiler needed, should be platform independent',
+ 'Interface Style' =>
+ 'Object oriented using blessed references and/or inheritance',
+ 'Public License' => 'Unknown',
+ ### XXX we can't really know what you have installed ###
+ #'Version Installed' => '0.06',
+ };
+
+ my $res = $Mod->details;
+
+ ### delete they key of which we don't know the value ###
+ delete $res->{'Version Installed'};
+
+ is_deeply( $res, $href, "Details OK" );
+}
+
+{ ### contians() test ###
+ ### XXX ->contains works based on package name. in our sourcefiles
+ ### we use 4x the same package name for different modules. So use
+ ### the only unique package name here, which is the one for the core mod
+ my @list = $CoreMod->contains;
+
+ ok( scalar(@list), "Found modules contained in this one" );
+ is_deeply( \@list, [$CoreMod],
+ " Found all modules expected" );
+}
+
+{ ### testing distributions() ###
+ my @mdists = $Mod->distributions;
+ is( scalar @mdists, 1, "Distributions found via module" );
+
+ my @adists = $Auth->distributions;
+ is( scalar @adists, 3, "Distributions found via author" );
+}
+
+{ ### test status->flush ###
+ ok( $Mod->status->mk_flush,
+ "Status flushed" );
+ ok(!$Mod->status->fetch," Fetch status empty" );
+ ok(!$Mod->status->extract,
+ " Extract status empty" );
+ ok(!$Mod->status->checksums,
+ " Checksums status empty" );
+ ok(!$Mod->status->readme,
+ " Readme status empty" );
+}
+
+{ ### testing bundles ###
+ my $bundle = $CB->module_tree('Bundle::Foo::Bar');
+ isa_ok( $bundle, 'CPANPLUS::Module' );
+
+ ok( $bundle->is_bundle, " It's a Bundle:: module" );
+ ok( $bundle->fetch, " Fetched the bundle" );
+ ok( $bundle->extract, " Extracted the bundle" );
+
+ my @objs = $bundle->bundle_modules;
+ is( scalar(@objs), 5, " Found all prerequisites" );
+
+ for( @objs ) {
+ isa_ok( $_, 'CPANPLUS::Module',
+ " Prereq " . $_->module );
+ ok( defined $bundle->status->prereqs->{$_->module},
+ " Prereq was registered" );
+ }
+}
+
+### test module from perl core ###
+{ isa_ok( $CoreMod, 'CPANPLUS::Module',
+ "Core module " . $CoreName );
+ ok( $CoreMod->package_is_perl_core,
+ " Package found in perl core" );
+
+ ### check if it's core with 5.6.1
+ { local $] = '5.006001';
+ ok( $CoreMod->module_is_supplied_with_perl_core,
+ " Module also found in perl core");
+ }
+
+ ok( !$CoreMod->install, " Package not installed" );
+ like( CPANPLUS::Error->stack_as_string, qr/core Perl/,
+ " Error properly logged" );
+}
+
+### test third-party modules
+SKIP: {
+ skip "Module::ThirdParty not installed", 10
+ unless eval { require Module::ThirdParty; 1 };
+
+ ok( !$Mod->is_third_party,
+ "Not a 3rd party module: ". $Mod->name );
+
+ my $fake = $CB->parse_module( module => 'LOCAL/SVN-Core-1.0' );
+ ok( $fake, "Created module object for ". $fake->name );
+ ok( $fake->is_third_party,
+ " It is a 3rd party module" );
+
+ my $info = $fake->third_party_information;
+ ok( $info, "Got 3rd party package information" );
+ isa_ok( $info, 'HASH' );
+
+ for my $item ( qw[name url author author_url] ) {
+ ok( length($info->{$item}),
+ " $item field is filled" );
+ }
+}
+
+### testing EU::Installed methods in Dist::MM tests ###
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/05_CPANPLUS-Internals-Fetch.t b/lib/CPANPLUS/t/05_CPANPLUS-Internals-Fetch.t
new file mode 100644
index 0000000000..f58b932c98
--- /dev/null
+++ b/lib/CPANPLUS/t/05_CPANPLUS-Internals-Fetch.t
@@ -0,0 +1,113 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+
+use CPANPLUS::Backend;
+
+use Test::More 'no_plan';
+use Data::Dumper;
+use File::Spec;
+use Cwd;
+use File::Basename;
+use CPANPLUS::Internals::Constants;
+
+my $conf = gimme_conf();
+
+### Redirect errors to file ###
+local $CPANPLUS::Error::ERROR_FH = output_handle() unless @ARGV;
+local $CPANPLUS::Error::MSG_FH = output_handle() unless @ARGV;
+
+my $cb = CPANPLUS::Backend->new( $conf );
+isa_ok($cb, "CPANPLUS::Internals" );
+
+my $mod = $cb->module_tree( TEST_CONF_MODULE );
+isa_ok( $mod, 'CPANPLUS::Module' );
+
+### fail host tests ###
+{ my $host = {};
+ my $rv = $cb->_add_fail_host( host => $host );
+
+ ok( $rv, "Failed host added " );
+ ok(!$cb->_host_ok( host => $host),
+ " Host registered as failed" );
+ ok( $cb->_host_ok( host => {} ),
+ " Fresh host unregistered" );
+}
+
+### refetch, even if it's there already ###
+{ my $where = $cb->_fetch( module => $mod, force => 1 );
+
+ ok( $where, "File downloaded to '$where'" );
+ ok( -s $where, " File exists" );
+ unlink $where;
+ ok(!-e $where, " File removed" );
+}
+
+### try to fetch something that doesn't exist ###
+{ ### set up a bogus host first ###
+ my $hosts = $conf->get_conf('hosts');
+ my $fail = { scheme => 'file',
+ path => "$0/$0" };
+
+ unshift @$hosts, $fail;
+ $conf->set_conf( hosts => $hosts );
+
+ ### the fallback host will get it ###
+ my $where = $cb->_fetch( module => $mod, force => 1, verbose => 0 );
+ ok($where, "File downloaded to '$where'" );
+ ok( -s $where, " File exists" );
+
+ ### but the error should be recorded ###
+ like( CPANPLUS::Error->stack_as_string, qr/Fetching of .*? failed/s,
+ " Error recorded appropriately" );
+
+ ### host marked as bad? ###
+ ok(!$cb->_host_ok( host => $fail ),
+ " Failed host logged properly" );
+
+ ### restore the hosts ###
+ shift @$hosts; $conf->set_conf( hosts => $hosts );
+}
+
+### try and fetch a URI
+{ my $base = basename($0);
+
+ ### do an ON_UNIX test, cygwin will fail tests otherwise (#14553)
+ ### create a file URI. Make sure to split it by LOCAL rules
+ ### and JOIN by unix rules, so we get a proper file uri
+ ### otherwise, we might break win32. See bug #18702
+ my $target = CREATE_FILE_URI->(
+ File::Spec::Unix->catfile(
+ File::Spec::Unix->catdir(
+ File::Spec->splitdir( cwd() ),
+ ),
+ $base
+ )
+ );
+
+ my $fake = $cb->parse_module( module => $target );
+
+ ok( IS_FAKE_MODOBJ->(mod => $fake),
+ "Fake module created from $0" );
+ is( $fake->status->_fetch_from, $target,
+ " Fetch from set ok" );
+
+ my $where = $fake->fetch;
+ ok( $where, " $target fetched ok" );
+ ok( -s $where, " $where exists" );
+ like( $where, '/'. UNKNOWN_DL_LOCATION .'/',
+ " Saved to proper location" );
+ like( $where, qr/$base$/, " Saved with proper name" );
+}
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/06_CPANPLUS-Internals-Constants.t b/lib/CPANPLUS/t/06_CPANPLUS-Internals-Constants.t
new file mode 100644
index 0000000000..2b09fe2241
--- /dev/null
+++ b/lib/CPANPLUS/t/06_CPANPLUS-Internals-Constants.t
@@ -0,0 +1,66 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use Test::More 'no_plan';
+use Cwd;
+use File::Basename;
+
+use CPANPLUS::Internals::Constants;
+use CPANPLUS::Module::Fake;
+use CPANPLUS::Module::Author::Fake;
+use CPANPLUS::Configure;
+use CPANPLUS::Backend;
+
+my $conf = gimme_conf();
+
+my $cb = CPANPLUS::Backend->new( $conf );
+
+my $f_auth = CPANPLUS::Module::Author::Fake->new( _id => $cb->_id );
+ok( $f_auth, "Fake auth object created" );
+ok( IS_AUTHOBJ->( $f_auth ), " IS_AUTHOBJ recognizes it" );
+ok( IS_FAKE_AUTHOBJ->( $f_auth ), " IS_FAKE_AUTHOBJ recognizes it" );
+
+my $f_mod = CPANPLUS::Module::Fake->new(
+ module => TEST_CONF_INST_MODULE ,
+ path => 'some/where',
+ package => 'Foo-Bar-1.2.tgz',
+ _id => $cb->_id,
+ );
+ok( $f_mod, "Fake mod object created" );
+ok( IS_MODOBJ->( $f_mod ), " IS_MODOBJ recognizes it" );
+ok( IS_FAKE_MODOBJ->( $f_mod ), " IS_FAKE_MODOJB recognizes it" );
+
+ok( IS_CONFOBJ->( conf => $conf ), "IS_CONFOBJ recognizes conf object" );
+
+ok( FILE_EXISTS->( file => basename($0) ), "FILE_EXISTS finds file" );
+ok( FILE_READABLE->( file => basename($0) ), "FILE_READABLE finds file" );
+ok( DIR_EXISTS->( dir => cwd() ), "DIR_EXISTS finds dir" );
+
+
+{ no strict 'refs';
+
+ my $tmpl = {
+ MAKEFILE_PL => 'Makefile.PL',
+ MAKEFILE => 'Makefile',
+ BUILD_PL => 'Build.PL',
+ BLIB => 'blib',
+ };
+
+ while ( my($sub,$res) = each %$tmpl ) {
+ is( &{$sub}->(), $res, "$sub returns proper result without args" );
+
+ my $long = File::Spec->catfile( cwd(), $res );
+ is( &{$sub}->( cwd() ), $long, "$sub returns proper result with args" );
+ }
+}
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/07_CPANPLUS-Internals-Extract.t b/lib/CPANPLUS/t/07_CPANPLUS-Internals-Extract.t
new file mode 100644
index 0000000000..b03befa8ac
--- /dev/null
+++ b/lib/CPANPLUS/t/07_CPANPLUS-Internals-Extract.t
@@ -0,0 +1,36 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+
+use CPANPLUS::Configure;
+use CPANPLUS::Backend;
+use CPANPLUS::Internals::Constants;
+use Test::More 'no_plan';
+use Data::Dumper;
+
+my $conf = gimme_conf();
+
+my $cb = CPANPLUS::Backend->new( $conf );
+
+### XXX SOURCEFILES FIX
+my $mod = $cb->module_tree( TEST_CONF_MODULE );
+
+isa_ok( $mod, 'CPANPLUS::Module' );
+
+my $where = $mod->fetch;
+ok( $where, "Module fetched" );
+
+my $dir = $cb->_extract( module => $mod );
+ok( $dir, "Module extracted" );
+ok( DIR_EXISTS->($dir), " Dir exists" );
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/08_CPANPLUS-Backend.t b/lib/CPANPLUS/t/08_CPANPLUS-Backend.t
new file mode 100644
index 0000000000..571a530284
--- /dev/null
+++ b/lib/CPANPLUS/t/08_CPANPLUS-Backend.t
@@ -0,0 +1,279 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use Test::More 'no_plan';
+use File::Basename 'dirname';
+
+use Data::Dumper;
+use CPANPLUS::Error;
+use CPANPLUS::Internals::Constants;
+
+my $conf = gimme_conf();
+
+### purposely avert messages and errors to a file? ###
+my $Trap_Output = @ARGV ? 0 : 1;
+
+my $Class = 'CPANPLUS::Backend';
+### D::C has troubles with the 'use_ok' -- it finds the wrong paths.
+### for now, do a 'use' instead
+#use_ok( $Class ) or diag "$Class not found";
+use CPANPLUS::Backend;
+
+my $cb = $Class->new( $conf );
+isa_ok( $cb, $Class );
+
+my $mt = $cb->module_tree;
+my $at = $cb->author_tree;
+ok( scalar keys %$mt, "Module tree has entries" );
+ok( scalar keys %$at, "Author tree has entries" );
+
+### module_tree tests ###
+my $Name = TEST_CONF_MODULE;
+my $mod = $cb->module_tree($Name);
+
+### XXX SOURCEFILES FIX
+{ my @mods = $cb->module_tree($Name,$Name);
+ my $none = $cb->module_tree( TEST_CONF_INVALID_MODULE );
+
+ ok( IS_MODOBJ->(mod => $mod), "Module object found" );
+ is( scalar(@mods), 2, " Module list found" );
+ ok( IS_MODOBJ->(mod => $mods[0]), " ISA module object" );
+ ok( !IS_MODOBJ->(mod => $none), " Bogus module detected");
+}
+
+### author_tree tests ###
+{ my @auths = $cb->author_tree( $mod->author->cpanid,
+ $mod->author->cpanid );
+ my $none = $cb->author_tree( 'fnurk' );
+
+ ok( IS_AUTHOBJ->(auth => $mod->author), "Author object found" );
+ is( scalar(@auths), 2, " Author list found" );
+ ok( IS_AUTHOBJ->( author => $auths[0] )," ISA author object" );
+ is( $mod->author, $auths[0], " Objects are identical" );
+ ok( !IS_AUTHOBJ->( author => $none ), " Bogus author detected" );
+}
+
+my $conf_obj = $cb->configure_object;
+ok( IS_CONFOBJ->(conf => $conf_obj), "Configure object found" );
+
+
+### parse_module tests ###
+{ my @map = ( # author package version
+ $Name => [ $mod->author->cpanid, $mod->package_name, $mod->version ],
+ $mod => [ $mod->author->cpanid, $mod->package_name, $mod->version ],
+ 'Foo-Bar-EU-NOXS'
+ => [ $mod->author->cpanid, $mod->package_name, $mod->version ],
+ 'Foo-Bar-EU-NOXS-0.01'
+ => [ $mod->author->cpanid, $mod->package_name, '0.01' ],
+ 'EUNOXS/Foo-Bar-EU-NOXS'
+ => [ 'EUNOXS', $mod->package_name, $mod->version ],
+ 'EUNOXS/Foo-Bar-EU-NOXS-0.01'
+ => [ 'EUNOXS', $mod->package_name, '0.01' ],
+ 'Foo-Bar-EU-NOXS-0.09'
+ => [ $mod->author->cpanid, $mod->package_name, '0.09' ],
+ 'MBXS/Foo-Bar-EU-NOXS-0.01'
+ => [ 'MBXS', $mod->package_name, '0.01' ],
+ 'EUNOXS/Foo-Bar-EU-NOXS-0.09'
+ => [ 'EUNOXS', $mod->package_name, '0.09' ],
+ 'EUNOXS/Foo-Bar-EU-NOXS-0.09.zip'
+ => [ 'EUNOXS', $mod->package_name, '0.09' ],
+ 'FROO/Flub-Flob-1.1.zip'
+ => [ 'FROO', 'Flub-Flob', '1.1' ],
+ 'G/GO/GOYALI/SMS_API_3_01.tar.gz'
+ => [ 'GOYALI', 'SMS_API', '3_01' ],
+ 'E/EY/EYCK/Net/Lite/Net-Lite-FTP-0.091'
+ => [ 'EYCK', 'Net-Lite-FTP', '0.091' ],
+ 'EYCK/Net/Lite/Net-Lite-FTP-0.091'
+ => [ 'EYCK', 'Net-Lite-FTP', '0.091' ],
+ 'M/MA/MAXDB/DBD-MaxDB-7.5.00.24a'
+ => [ 'MAXDB', 'DBD-MaxDB', '7.5.00.24a' ],
+ 'EUNOXS/perl5.005_03.tar.gz'
+ => [ 'EUNOXS', 'perl', '5.005_03' ],
+ 'FROO/Flub-Flob-v1.1.0.tbz'
+ => [ 'FROO', 'Flub-Flob', 'v1.1.0' ],
+ 'FROO/Flub-Flob-1.1_2.tbz'
+ => [ 'FROO', 'Flub-Flob', '1.1_2' ],
+ 'LDS/CGI.pm-3.27.tar.gz'
+ => [ 'LDS', 'CGI', '3.27' ],
+ 'FROO/Text-Tabs+Wrap-2006.1117.tar.gz'
+ => [ 'FROO', 'Text-Tabs+Wrap', '2006.1117' ],
+ 'JETTERO/Crypt-PBC-0.7.20.0-0.4.9',
+ => [ 'JETTERO', 'Crypt-PBC', '0.7.20.0-0.4.9' ],
+
+ );
+
+ while ( my($guess, $attr) = splice @map, 0, 2 ) {
+ my( $author, $pkg, $version ) = @$attr;
+
+ ok( $guess, "Attempting to parse $guess" );
+
+ my $obj = $cb->parse_module( module => $guess );
+
+ ok( $obj, " Result returned" );
+ ok( IS_MODOBJ->( mod => $obj ),
+ " parse_module success by '$guess'" );
+
+ is( $obj->version, $version,
+ " Proper version found: $version" );
+ is( $obj->package_version, $version,
+ " Found in package_version as well" );
+ is( $obj->package_name, $pkg,
+ " Proper package found: $pkg" );
+ unlike( $obj->package_name, qr/\d/,
+ " No digits in package name" );
+ like( $obj->author->cpanid, "/$author/i",
+ " Proper author found: $author");
+ like( $obj->path, "/$author/i",
+ " Proper path found: " . $obj->path );
+ }
+
+
+ ### test for things that look like real modules, but aren't ###
+ { local $CPANPLUS::Error::MSG_FH = output_handle() if $Trap_Output;
+ local $CPANPLUS::Error::ERROR_FH = output_handle() if $Trap_Output;
+
+ my @map = (
+ [ $Name . $$ => [
+ [qr/does not contain an author/,"Missing author part detected"],
+ [qr/Cannot find .+? in the module tree/,"Unable to find module"]
+ ] ],
+ [ {}, => [
+ [ qr/module string from reference/,"Unable to parse ref"]
+ ] ],
+ );
+
+ for my $entry ( @map ) {
+ my($mod,$aref) = @$entry;
+
+ my $none = $cb->parse_module( module => $mod );
+ ok( !IS_MODOBJ->(mod => $none),
+ "Non-existant module detected" );
+ ok( !IS_FAKE_MODOBJ->(mod => $none),
+ "Non-existant fake module detected" );
+
+ my $str = CPANPLUS::Error->stack_as_string;
+ for my $pair (@$aref) {
+ my($re,$diag) = @$pair;
+ like( $str, $re," $diag" );
+ }
+ }
+ }
+
+ ### test parsing of arbitrary URI
+ for my $guess ( qw[ http://foo/bar.gz
+ http://a/b/c/d/e/f/g/h/i/j
+ flub://floo ]
+ ) {
+ my $obj = $cb->parse_module( module => $guess );
+ ok( IS_FAKE_MODOBJ->(mod => $obj), "parse_module success by '$guess'" );
+ is( $obj->status->_fetch_from, $guess,
+ " Fetch from set ok" );
+ }
+}
+
+### RV tests ###
+{ my $method = 'readme';
+ my %args = ( modules => [$Name] );
+
+ my $rv = $cb->$method( %args );
+ ok( IS_RVOBJ->( $rv ), "Got an RV object" );
+ ok( $rv->ok, " Overall OK" );
+ cmp_ok( $rv, '==', 1, " Overload OK" );
+ is( $rv->function, $method, " Function stored OK" );
+ is_deeply( $rv->args, \%args, " Arguments stored OK" );
+ is( $rv->rv->{$Name}, $mod->readme, " RV as expected" );
+}
+
+### reload_indices tests ###
+{
+ my $file = File::Spec->catfile( $conf->get_conf('base'),
+ $conf->_get_source('mod'),
+ );
+
+ ok( $cb->reload_indices( update_source => 0 ), "Rebuilding trees" );
+ my $age = -M $file;
+
+ ### make sure we are 'newer' on faster machines with a sleep..
+ ### apparently Win32's FAT isn't granual enough on intervals
+ ### < 2 seconds, so it may give the same answer before and after
+ ### the sleep, causing the test to fail. so sleep atleast 2 seconds.
+ sleep 2;
+ ok( $cb->reload_indices( update_source => 1 ),
+ "Rebuilding and refetching trees" );
+ cmp_ok( $age, '>', -M $file, " Source file '$file' updated" );
+}
+
+### flush tests ###
+{
+ for my $cache( qw[methods hosts modules lib all] ) {
+ ok( $cb->flush($cache), "Cache $cache flushed ok" );
+ }
+}
+
+### installed tests ###
+{
+ ok( scalar $cb->installed, "Found list of installed modules" );
+}
+
+### autobudle tests ###
+{
+ my $where = $cb->autobundle;
+ ok( $where, "Autobundle written" );
+ ok( -s $where, " File has size" );
+}
+
+### local_mirror tests ###
+{ ### turn off md5 checks for the 'fake' packages we have
+ my $old_md5 = $conf->get_conf('md5');
+ $conf->set_conf( md5 => 0 );
+
+ ### otherwise 'status->fetch' might be undef! ###
+ my $rv = $cb->local_mirror( path => 'dummy-localmirror' );
+ ok( $rv, "Local mirror created" );
+
+ for my $mod ( values %{ $cb->module_tree } ) {
+ my $name = $mod->module;
+
+ my $cksum = File::Spec->catfile(
+ dirname($mod->status->fetch),
+ CHECKSUMS );
+ ok( -e $mod->status->fetch, " Module '$name' fetched" );
+ ok( -s _, " Module '$name' has size" );
+ ok( -e $cksum, " Checksum fetched for '$name'" );
+ ok( -s _, " Checksum for '$name' has size" );
+ }
+
+ $conf->set_conf( md5 => $old_md5 );
+}
+
+### check ENV variable
+{ ### process id
+ { my $name = 'PERL5_CPANPLUS_IS_RUNNING';
+ ok( $ENV{$name}, "Env var '$name' set" );
+ is( $ENV{$name}, $$, " Set to current process id" );
+ }
+
+ ### Version
+ { my $name = 'PERL5_CPANPLUS_IS_VERSION';
+ ok( $ENV{$name}, "Env var '$name' set" );
+
+ ### version.pm formats ->VERSION output... *sigh*
+ is( $ENV{$name}, $Class->VERSION,
+ " Set to current process version" );
+ }
+
+}
+
+__END__
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
diff --git a/lib/CPANPLUS/t/09_CPANPLUS-Internals-Search.t b/lib/CPANPLUS/t/09_CPANPLUS-Internals-Search.t
new file mode 100644
index 0000000000..583d464740
--- /dev/null
+++ b/lib/CPANPLUS/t/09_CPANPLUS-Internals-Search.t
@@ -0,0 +1,78 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use Test::More 'no_plan';
+use Data::Dumper;
+use CPANPLUS::Backend;
+use CPANPLUS::Internals::Constants;
+
+my $Conf = gimme_conf();
+my $CB = CPANPLUS::Backend->new($Conf);
+my $ModName = TEST_CONF_MODULE;
+my $Mod = $CB->module_tree( $ModName );
+
+
+### search for modules ###
+for my $type ( CPANPLUS::Module->accessors() ) {
+
+ ### don't muck around with references/objects
+ ### or private identifiers
+ next if ref $Mod->$type() or $type =~/^_/;
+
+ my @aref = $CB->search(
+ type => $type,
+ allow => [$Mod->$type()],
+ );
+
+ ok( scalar @aref, "Module found by '$type'" );
+ for( @aref ) {
+ ok( IS_MODOBJ->($_)," Module isa module object" );
+ }
+}
+
+### search for authors ###
+my $auth = $Mod->author;
+for my $type ( CPANPLUS::Module::Author->accessors() ) {
+ my @aref = $CB->search(
+ type => $type,
+ allow => [$auth->$type()],
+ );
+
+ ok( @aref, "Author found by '$type'" );
+ for( @aref ) {
+ ok( IS_AUTHOBJ->($_), " Author isa author object" );
+ }
+}
+
+
+{ my $warning = '';
+ local $SIG{__WARN__} = sub { $warning .= "@_"; };
+
+ { ### try search that will yield nothing ###
+ ### XXX SOURCEFILES FIX
+ my @list = $CB->search( type => 'module',
+ allow => [$ModName.$$] );
+
+ is( scalar(@list), 0, "Valid search yields no results" );
+ is( $warning, '', " No warnings issued" );
+ }
+
+ { ### try bogus arguments ###
+ my @list = $CB->search( type => '', allow => ['foo'] );
+
+ is( scalar(@list), 0, "Broken search yields no results" );
+ like( $warning, qr/^Key 'type'.* is of invalid type for/,
+ " Got a warning for wrong arguments" );
+ }
+}
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/10_CPANPLUS-Error.t b/lib/CPANPLUS/t/10_CPANPLUS-Error.t
new file mode 100644
index 0000000000..8a954e7779
--- /dev/null
+++ b/lib/CPANPLUS/t/10_CPANPLUS-Error.t
@@ -0,0 +1,113 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use Test::More 'no_plan';
+use Data::Dumper;
+use FileHandle;
+use CPANPLUS::Error;
+
+my $conf = gimme_conf();
+
+my $map = {
+ cp_msg => ["This is just a test message"],
+ msg => ["This is just a test message"],
+ cp_error => ["This is just a test error"],
+ error => ["This is just a test error"],
+};
+
+### check if CPANPLUS::Error can do what we expect
+{ for my $name ( keys %$map ) {
+ can_ok('CPANPLUS::Error', $name);
+ can_ok('main', $name); # did it get exported?
+ }
+}
+
+### make sure we start with an empty stack
+{ CPANPLUS::Error->flush;
+ is( scalar(()=CPANPLUS::Error->stack), 0,
+ "Starting with empty stack" );
+}
+
+### global variables test ###
+{ my $file = output_file();
+ unlink $file; # just in case
+
+ local $CPANPLUS::Error::MSG_FH = output_handle();
+ local $CPANPLUS::Error::ERROR_FH = output_handle();
+
+ ok( -e $file, "Output redirect file exists" );
+ ok( !-s $file, " Output file is empty" );
+
+ ### print a msg & error ###
+ for my $name ( keys %$map ) {
+ my $sub = __PACKAGE__->can( $name );
+
+ $sub->( $map->{$name}->[0], 1 );
+ }
+
+ ### must close it for Win32 tests!
+ close output_handle;
+
+ ok( -s $file, " Output file now has size" );
+
+ my $fh = FileHandle->new( $file );
+ ok( $fh, "Opened output file for reading " );
+
+ my $contents = do { local $/; <$fh> };
+ my $string = CPANPLUS::Error->stack_as_string;
+ my $trace = CPANPLUS::Error->stack_as_string(1);
+
+ ok( $contents, " Got the file contents" );
+ ok( $string, "Got the error stack as string" );
+
+
+ for my $type ( keys %$map ) {
+ my $tag = $type; $tag =~ s/.+?_//g;
+
+ for my $str (@{ $map->{$type} } ) {
+ like( $contents, qr/\U\Q$tag/,
+ " Contents matches for '$type'" );
+ like( $contents, qr/\Q$str/,
+ " Contents matches for '$type'" );
+
+ like( $string, qr/\U\Q$tag/,
+ " String matches for '$type'" );
+ like( $string, qr/\Q$str/,
+ " String matches for '$type'" );
+
+ like( $trace, qr/\U\Q$tag/,
+ " Trace matches for '$type'" );
+ like( $trace, qr/\Q$str/,
+ " Trace matches for '$type'" );
+
+ ### extra trace tests ###
+ like( $trace, qr/\Q$str\E.*?\Q$str/s,
+ " Trace holds proper traceback" );
+ like( $trace, qr/\Q$0/,
+ " Trace holds program name" );
+ like( $trace, qr/line/,
+ " Trace holds line number information" );
+ }
+ }
+
+ ### check the stack, flush it, check again ###
+ is( scalar(()=CPANPLUS::Error->stack), scalar(keys(%$map)),
+ "All items on stack" );
+ is( scalar(()=CPANPLUS::Error->flush), scalar(keys(%$map)),
+ "All items flushed" );
+ is( scalar(()=CPANPLUS::Error->stack), 0,
+ "No items on stack" );
+
+}
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/19_CPANPLUS-Dist.t b/lib/CPANPLUS/t/19_CPANPLUS-Dist.t
new file mode 100644
index 0000000000..4c48fcaab3
--- /dev/null
+++ b/lib/CPANPLUS/t/19_CPANPLUS-Dist.t
@@ -0,0 +1,417 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+### dummy class for testing dist api ###
+BEGIN {
+
+ package CPANPLUS::Dist::_Test;
+ use strict;
+ use vars qw[$Available $Create $Install $Init $Prepare @ISA];
+
+ @ISA = qw[CPANPLUS::Dist];
+ $Available = 1;
+ $Create = 1;
+ $Install = 1;
+ $Init = 1;
+ $Prepare = 1;
+
+ require CPANPLUS::Dist;
+ CPANPLUS::Dist->_add_dist_types( __PACKAGE__ );
+
+ sub init { $_[0]->status->mk_accessors(
+ qw[prepared created installed
+ _prepare_args _install_args _create_args]);
+ return $Init };
+ sub format_available { return $Available }
+ sub prepare { return shift->status->prepared($Prepare) }
+ sub create { return shift->status->created($Create) }
+ sub install { return shift->status->installed($Install) }
+}
+
+use strict;
+
+use CPANPLUS::Configure;
+use CPANPLUS::Backend;
+use CPANPLUS::Internals::Constants;
+
+use Test::More 'no_plan';
+use Cwd;
+use Data::Dumper;
+use File::Basename ();
+use File::Spec ();
+use Module::Load::Conditional qw[check_install];
+
+my $conf = gimme_conf();
+my $cb = CPANPLUS::Backend->new( $conf );
+
+### Redirect errors to file ###
+local $CPANPLUS::Error::ERROR_FH = output_handle() unless @ARGV;
+local $CPANPLUS::Error::MSG_FH = output_handle() unless @ARGV;
+
+### obsolete
+#my $Format = '_test';
+my $Module = 'CPANPLUS::Dist::_Test';
+my $ModName = TEST_CONF_MODULE;
+my $ModPrereq = TEST_CONF_INST_MODULE;
+### XXX this version doesn't exist, but we don't check for it either ###
+my $Prereq = { $ModPrereq => '1000' };
+
+### since it's in this file, not in it's own module file,
+### make M::L::C think it already was loaded
+$Module::Load::Conditional::CACHE->{$Module}->{usable} = 1;
+
+
+use_ok('CPANPLUS::Dist');
+
+### start with fresh sources ###
+ok( $cb->reload_indices( update_source => 0 ),
+ "Rebuilding trees" );
+
+my $Mod = $cb->module_tree( $ModName );
+ok( $Mod, "Got module object" );
+
+
+### straight forward dist build - prepare, create, install
+{ my $dist = CPANPLUS::Dist->new(
+ format => $Module,
+ module => $Mod
+ );
+
+ ok( $dist, "New dist object created" );
+ isa_ok( $dist, 'CPANPLUS::Dist' );
+ isa_ok( $dist, $Module );
+
+ my $status = $dist->status;
+ ok( $status, "Status object found" );
+ isa_ok( $status, "Object::Accessor" );
+
+ ok( $dist->prepare, "Prepare call" );
+ ok( $dist->status->prepared," Status registered OK" );
+
+ ok( $dist->create, "Create call" );
+ ok( $dist->status->created, " Status registered OK" );
+
+ ok( $dist->install, "Install call" );
+ ok( $dist->status->installed,
+ " Status registered OK" );
+}
+
+### check 'sanity check' option ###
+{ local $CPANPLUS::Dist::_Test::Available = 0;
+
+ ok( !$Module->format_available,
+ "Format availabillity turned off" );
+
+ { $conf->_set_build('sanity_check' => 0);
+
+ my $dist = CPANPLUS::Dist->new(
+ format => $Module,
+ module => $Mod
+ );
+
+ ok( $dist, "Dist created with sanity check off" );
+ isa_ok( $dist, $Module );
+
+ }
+
+ { $conf->_set_build('sanity_check' => 1);
+ my $dist = CPANPLUS::Dist->new(
+ format => $Module,
+ module => $Mod
+ );
+
+ ok( !$dist, "Dist not created with sanity check on" );
+ like( CPANPLUS::Error->stack_as_string,
+ qr/Format '$Module' is not available/,
+ " Error recorded as expected" );
+ }
+}
+
+### undef the status hash, make sure it complains ###
+{ local $CPANPLUS::Dist::_Test::Init = 0;
+
+ my $dist = CPANPLUS::Dist->new(
+ format => $Module,
+ module => $Mod
+ );
+
+ ok( !$dist, "No dist created by failed init" );
+ like( CPANPLUS::Error->stack_as_string,
+ qr/Dist initialization of '$Module' failed for/s,
+ " Error recorded as expected" );
+}
+
+### test _resolve prereqs, in a somewhat simulated set of circumstances
+{ my $old_prereq = $conf->get_conf('prereqs');
+
+ my $map = {
+ 0 => {
+ 'Previous install failed' => [
+ sub { $cb->module_tree($ModPrereq)->status->installed(0);
+ 'install' },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/failed to install before in this session/s,
+ " Previous install failed recorded ok" ) },
+ ],
+
+ "Set $Module->prepare to false" => [
+ sub { $CPANPLUS::Dist::_Test::Prepare = 0; 'install' },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Unable to create a new distribution object/s,
+ " Dist creation failed recorded ok" ) },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Failed to install '$ModPrereq' as prerequisite/s,
+ " Dist creation failed recorded ok" ) },
+ ],
+
+ "Set $Module->create to false" => [
+ sub { $CPANPLUS::Dist::_Test::Create = 0; 'install' },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Unable to create a new distribution object/s,
+ " Dist creation failed recorded ok" ) },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Failed to install '$ModPrereq' as prerequisite/s,
+ " Dist creation failed recorded ok" ) },
+ ],
+
+ "Set $Module->install to false" => [
+ sub { $CPANPLUS::Dist::_Test::Install = 0; 'install' },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Failed to install '$ModPrereq' as/s,
+ " Dist installation failed recorded ok" ) },
+ ],
+
+ "Set dependency to be perl-core" => [
+ sub { $cb->module_tree( $ModPrereq )->package(
+ 'perl-5.8.1.tar.gz' ); 'install' },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Prerequisite '$ModPrereq' is perl-core/s,
+ " Dist installation failed recorded ok" ) },
+ ],
+ 'Simple ignore' => [
+ sub { 'ignore' },
+ sub { ok( !$_[0]->status->prepared,
+ " Module status says not prepared" ) },
+ sub { ok( !$_[0]->status->created,
+ " Module status says not created" ) },
+ sub { ok( !$_[0]->status->installed,
+ " Module status says not installed" ) },
+ ],
+ 'Ignore from conf' => [
+ sub { $conf->set_conf(prereqs => PREREQ_IGNORE); '' },
+ sub { ok( !$_[0]->status->prepared,
+ " Module status says not prepared" ) },
+ sub { ok( !$_[0]->status->created,
+ " Module status says not created" ) },
+ sub { ok( !$_[0]->status->installed,
+ " Module status says not installed" ) },
+ ### set the conf back ###
+ sub { $conf->set_conf(prereqs => PREREQ_INSTALL); },
+ ],
+ },
+ 1 => {
+ 'Simple create' => [
+ sub { 'create' },
+ sub { ok( $_[0]->status->prepared,
+ " Module status says prepared" ) },
+ sub { ok( $_[0]->status->created,
+ " Module status says created" ) },
+ sub { ok( !$_[0]->status->installed,
+ " Module status says not installed" ) },
+ ],
+ 'Simple install' => [
+ sub { 'install' },
+ sub { ok( $_[0]->status->prepared,
+ " Module status says prepared" ) },
+ sub { ok( $_[0]->status->created,
+ " Module status says created" ) },
+ sub { ok( $_[0]->status->installed,
+ " Module status says installed" ) },
+ ],
+
+ 'Install from conf' => [
+ sub { $conf->set_conf(prereqs => PREREQ_INSTALL); '' },
+ sub { ok( $_[0]->status->prepared,
+ " Module status says prepared" ) },
+ sub { ok( $_[0]->status->created,
+ " Module status says created" ) },
+ sub { ok( $_[0]->status->installed,
+ " Module status says installed" ) },
+ ],
+ 'Create from conf' => [
+ sub { $conf->set_conf(prereqs => PREREQ_BUILD); '' },
+ sub { ok( $_[0]->status->prepared,
+ " Module status says prepared" ) },
+ sub { ok( $_[0]->status->created,
+ " Module status says created" ) },
+ sub { ok( !$_[0]->status->installed,
+ " Module status says not installed" ) },
+ ### set the conf back ###
+ sub { $conf->set_conf(prereqs => PREREQ_INSTALL); },
+ ],
+
+ 'Ask from conf' => [
+ sub { $cb->_register_callback(
+ name => 'install_prerequisite',
+ code => sub {1} );
+ $conf->set_conf(prereqs => PREREQ_ASK); '' },
+ sub { ok( $_[0]->status->prepared,
+ " Module status says prepared" ) },
+ sub { ok( $_[0]->status->created,
+ " Module status says created" ) },
+ sub { ok( $_[0]->status->installed,
+ " Module status says installed" ) },
+ ### set the conf back ###
+ sub { $conf->set_conf(prereqs => PREREQ_INSTALL); },
+
+ ],
+
+ 'Ask from conf, but decline' => [
+ sub { $cb->_register_callback(
+ name => 'install_prerequisite',
+ code => sub {0} );
+ $conf->set_conf( prereqs => PREREQ_ASK); '' },
+ sub { ok( !$_[0]->status->installed,
+ " Module status says not installed" ) },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Will not install prerequisite '$ModPrereq' -- Note/,
+ " Install skipped, recorded ok" ) },
+ ### set the conf back ###
+ sub { $conf->set_conf(prereqs => PREREQ_INSTALL); },
+ ],
+
+ "Set recursive dependency" => [
+ sub { $cb->_status->pending_prereqs({ $ModPrereq => 1 });
+ 'install' },
+ sub { like( CPANPLUS::Error->stack_as_string,
+ qr/Recursive dependency detected/,
+ " Recursive dependency recorded ok" ) },
+ ],
+
+ },
+ };
+
+ for my $bool ( sort keys %$map ) {
+
+ diag("Running ". ($bool?'success':'fail') . " tests") if @ARGV;
+
+ my $href = $map->{$bool};
+ while ( my($txt,$aref) = each %$href ) {
+
+ ### reset everything ###
+ ok( $cb->reload_indices( update_source => 0 ),
+ "Rebuilding trees" );
+
+ $CPANPLUS::Dist::_Test::Available = 1;
+ $CPANPLUS::Dist::_Test::Prepare = 1;
+ $CPANPLUS::Dist::_Test::Create = 1;
+ $CPANPLUS::Dist::_Test::Install = 1;
+
+ CPANPLUS::Error->flush;
+ $cb->_status->mk_flush;
+
+ ### get a new dist from Text::Bastardize ###
+ my $dist = CPANPLUS::Dist->new(
+ format => $Module,
+ module => $cb->module_tree( $ModName ),
+ );
+
+ ### first sub returns target ###
+ my $sub = shift @$aref;
+ my $target = $sub->();
+
+ my $flag = $dist->_resolve_prereqs(
+ format => $Module,
+ force => 1,
+ target => $target,
+ prereqs => $Prereq );
+
+ is( !!$flag, !!$bool, $txt );
+
+ ### any extra tests ###
+ $_->($cb->module_tree($ModPrereq)) for @$aref;
+
+ }
+ }
+}
+
+
+### prereq satisfied tests
+{ my $map = {
+ # version regex
+ 0 => undef,
+ 1 => undef,
+ 2 => qr/have to resolve/,
+ };
+
+ my $mod = CPANPLUS::Module::Fake->new(
+ module => $$,
+ package => $$,
+ path => $$,
+ version => 1 );
+
+ ok( $mod, "Fake module created" );
+ is( $mod->version, 1, " Version set correctly" );
+
+ my $dist = CPANPLUS::Dist->new(
+ format => $Module,
+ module => $Mod
+ );
+
+ ok( $dist, "Dist object created" );
+ isa_ok( $dist, $Module );
+
+
+ ### scope it for the locals
+ { local $^W; # quell sub redefined warnings;
+
+ ### is_uptodate will need to return false for this test
+ local *CPANPLUS::Module::Fake::is_uptodate = sub { return };
+ local *CPANPLUS::Module::Fake::is_uptodate = sub { return };
+ CPANPLUS::Error->flush;
+
+
+ ### it's satisfied
+ while( my($ver, $re) = each %$map ) {
+
+ my $rv = $dist->prereq_satisfied(
+ version => $ver,
+ modobj => $mod );
+
+ ok( 1, "Testing ver: $ver" );
+ is( $rv, undef, " Return value as expected" );
+
+ if( $re ) {
+ like( CPANPLUS::Error->stack_as_string, $re,
+ " Error as expected" );
+ }
+
+ CPANPLUS::Error->flush;
+ }
+ }
+}
+
+
+### dist_types tests
+{ can_ok( 'CPANPLUS::Dist', 'dist_types' );
+
+ SKIP: {
+ skip "You do not have Module::Pluggable installed", 2
+ unless check_install( module => 'Module::Pluggable' );
+
+ my @types = CPANPLUS::Dist->dist_types;
+ ok( scalar(@types), " Dist types found" );
+ ok( grep( /_Test/, @types), " Found our _Test dist type" );
+ }
+}
+1;
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/20_CPANPLUS-Dist-MM.t b/lib/CPANPLUS/t/20_CPANPLUS-Dist-MM.t
new file mode 100644
index 0000000000..9516cc0d50
--- /dev/null
+++ b/lib/CPANPLUS/t/20_CPANPLUS-Dist-MM.t
@@ -0,0 +1,403 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+
+use CPANPLUS::Configure;
+use CPANPLUS::Backend;
+use CPANPLUS::Dist;
+use CPANPLUS::Dist::MM;
+use CPANPLUS::Internals::Constants;
+
+use Test::More 'no_plan';
+use Cwd;
+use Config;
+use Data::Dumper;
+use File::Basename ();
+use File::Spec ();
+
+my $conf = gimme_conf();
+my $cb = CPANPLUS::Backend->new( $conf );
+my $noperms = ($< and not $conf->get_program('sudo')) &&
+ ($conf->get_conf('makemakerflags') or
+ not -w $Config{installsitelib} );
+my $File = 'Bar.pm';
+my $Verbose = @ARGV ? 1 : 0;
+
+#$IPC::Cmd::DEBUG = $Verbose;
+
+### Make sure we get the _EUMM_NOXS_ version
+my $ModName = TEST_CONF_MODULE;
+
+### This is the module name that gets /installed/
+my $InstName = TEST_CONF_INST_MODULE;
+
+### don't start sending test reports now... ###
+$cb->_callbacks->send_test_report( sub { 0 } );
+$conf->set_conf( cpantest => 0 );
+
+### Redirect errors to file ###
+local $CPANPLUS::Error::ERROR_FH = output_handle() unless $Verbose;
+local $CPANPLUS::Error::MSG_FH = output_handle() unless $Verbose;
+*STDERR = output_handle() unless $Verbose;
+
+### dont uncomment this, it screws up where STDOUT goes and makes
+### test::harness create test counter mismatches
+#*STDOUT = output_handle() unless @ARGV;
+### for the same test-output counter mismatch, we disable verbose
+### mode
+$conf->set_conf( verbose => $Verbose );
+$conf->set_conf( allow_build_interactivity => 0 );
+
+### start with fresh sources ###
+ok( $cb->reload_indices( update_source => 0 ),
+ "Rebuilding trees" );
+
+### we might need this Some Day when we're going to install into
+### our own sandbox dir.. but for now, no dice due to EU::I bug
+# $conf->set_program( sudo => '' );
+# $conf->set_conf( makemakerflags => TEST_INSTALL_EU_MM_FLAGS );
+
+### set alternate install dir ###
+### XXX rather pointless, since we can't uninstall them, due to a bug
+### in EU::Installed (6871). And therefor we can't test uninstall() or any of
+### the EU::Installed functions. So, let's just install into sitelib... =/
+#my $prefix = File::Spec->rel2abs( File::Spec->catdir(cwd(),'dummy-perl') );
+#my $rv = $cb->configure_object->set_conf( makemakerflags => "PREFIX=$prefix" );
+#ok( $rv, "Alternate install path set" );
+
+my $Mod = $cb->module_tree( $ModName );
+my $InstMod = $cb->module_tree( $InstName );
+ok( $Mod, "Loaded object for: " . $Mod->name );
+ok( $Mod, "Loaded object for: " . $InstMod->name );
+
+### format_available tests ###
+{ ok( CPANPLUS::Dist::MM->format_available,
+ "Format is available" );
+
+ ### whitebox test!
+ { local $^W;
+ local *CPANPLUS::Dist::MM::can_load = sub { 0 };
+ ok(!CPANPLUS::Dist::MM->format_available,
+ " Making format unavailable" );
+ }
+
+ ### test if the error got logged ok ###
+ like( CPANPLUS::Error->stack_as_string,
+ qr/You do not have .+?'CPANPLUS::Dist::MM' not available/s,
+ " Format failure logged" );
+
+ ### flush the stack ###
+ CPANPLUS::Error->flush;
+}
+
+ok( $Mod->fetch, "Fetching module to ".$Mod->status->fetch );
+ok( $Mod->extract, "Extracting module to ".$Mod->status->extract );
+
+ok( $Mod->test, "Testing module" );
+
+ok( $Mod->status->dist_cpan->status->test,
+ " Test success registered as status" );
+ok( $Mod->status->dist_cpan->status->prepared,
+ " Prepared status registered" );
+ok( $Mod->status->dist_cpan->status->created,
+ " Created status registered" );
+is( $Mod->status->dist_cpan->status->distdir, $Mod->status->extract,
+ " Distdir status registered properly" );
+
+### test the convenience methods
+ok( $Mod->prepare, "Preparing module" );
+ok( $Mod->create, "Creating module" );
+
+ok( $Mod->dist, "Building distribution" );
+ok( $Mod->status->dist_cpan, " Dist registered as status" );
+isa_ok( $Mod->status->dist_cpan, "CPANPLUS::Dist::MM" );
+
+### flush the lib cache
+### otherwise, cpanplus thinks the module's already installed
+### since the blib is already in @INC
+$cb->_flush( list => [qw|lib|] );
+
+SKIP: {
+
+ skip(q[No install tests under core perl], 10) if $ENV{PERL_CORE};
+
+ skip(q[Probably no permissions to install, skipping], 10)
+ if $noperms;
+
+ ### XXX new EU::I should be forthcoming pending this patch from Steffen
+ ### Mueller on p5p: http://www.xray.mpe.mpg.de/mailing-lists/ \
+ ### perl5-porters/2007-01/msg00895.html
+ ### This should become EU::I 1.42.. if so, we should upgrade this bit of
+ ### code and remove the diag, since we can then install in our dummy dir..
+ diag("\nSorry, installing into your real perl dir, rather than our test");
+ diag("area since ExtUtils::Installed does not probe for .packlists in " );
+ diag('other dirs than those in %Config. See bug #6871 on rt.cpan.org ' );
+ diag('for details');
+
+ diag(q[Note: 'sudo' might ask for your password to do the install test])
+ if $conf->get_program('sudo');
+
+ ok( $Mod->install( force =>1 ),
+ "Installing module" );
+ ok( $Mod->status->installed," Module installed according to status" );
+
+
+ SKIP: { ### EU::Installed tests ###
+
+ skip("makemakerflags set -- probably EU::Installed tests will fail", 8)
+ if $conf->get_conf('makemakerflags');
+
+ skip( "Old perl on cygwin detected " .
+ "-- tests will fail due to known bugs", 8
+ ) if ON_OLD_CYGWIN;
+
+ ### might need it Later when EU::I is fixed..
+ #local @INC = ( TEST_INSTALL_DIR_LIB, @INC );
+
+ { ### validate
+ my @missing = $InstMod->validate;
+
+ is_deeply( \@missing, [],
+ "No missing files" );
+ }
+
+ { ### files
+ my @files = $InstMod->files;
+
+ ### number of files may vary from OS to OS
+ ok( scalar(@files), "All files accounted for" );
+ ok( grep( /$File/, @files),
+ " Found the module" );
+
+ ### XXX does this work on all OSs?
+ #ok( grep( /man/, @files ),
+ # " Found the manpage" );
+ }
+
+ { ### packlist
+ my ($obj) = $InstMod->packlist;
+ isa_ok( $obj, "ExtUtils::Packlist" );
+ }
+
+ { ### directory_tree
+ my @dirs = $InstMod->directory_tree;
+ ok( scalar(@dirs), "Directory tree obtained" );
+
+ my $found;
+ for my $dir (@dirs) {
+ ok( -d $dir, " Directory exists" );
+
+ my $file = File::Spec->catfile( $dir, $File );
+ $found = $file if -e $file;
+ }
+
+ ok( -e $found, " Module found" );
+ }
+
+ SKIP: {
+ skip("Probably no permissions to uninstall", 1)
+ if $noperms;
+
+ ok( $InstMod->uninstall,"Uninstalling module" );
+ }
+ }
+}
+
+### test exceptions in Dist::MM->create ###
+{ ok( $Mod->status->mk_flush, "Old status info flushed" );
+ my $dist = CPANPLUS::Dist->new( module => $Mod,
+ format => INSTALLER_MM );
+
+ ok( $dist, "New dist object made" );
+ ok(!$dist->prepare, " Dist->prepare failed" );
+ like( CPANPLUS::Error->stack_as_string, qr/No dir found to operate on/,
+ " Failure logged" );
+
+ ### manually set the extract dir,
+ $Mod->status->extract($0);
+
+ ok(!$dist->create, " Dist->create failed" );
+ like( CPANPLUS::Error->stack_as_string, qr/not successfully prepared/s,
+ " Failure logged" );
+
+ ### pretend we've been prepared ###
+ $dist->status->prepared(1);
+
+ ok(!$dist->create, " Dist->create failed" );
+ like( CPANPLUS::Error->stack_as_string, qr/Could not chdir/s,
+ " Failure logged" );
+}
+
+### writemakefile.pl tests ###
+{ ### remove old status info
+ ok( $Mod->status->mk_flush, "Old status info flushed" );
+ ok( $Mod->fetch, "Module fetched again" );
+ ok( $Mod->extract, "Module extracted again" );
+
+ ### cheat and add fake prereqs ###
+ my $prereq = TEST_CONF_PREREQ;
+
+ $Mod->status->prereqs( { $prereq => 0 } );
+
+ my $makefile_pl = MAKEFILE_PL->( $Mod->status->extract );
+ my $makefile = MAKEFILE->( $Mod->status->extract );
+
+ my $dist = $Mod->dist;
+ ok( $dist, "Dist object built" );
+
+ ### check for a makefile.pl and 'write' one
+ ok( -s $makefile_pl, " Makefile.PL present" );
+ ok( $dist->write_makefile_pl( force => 0 ),
+ " Makefile.PL written" );
+ like( CPANPLUS::Error->stack_as_string, qr/Already created/,
+ " Prior existance noted" );
+
+ ### ok, unlink the makefile.pl, now really write one
+ unlink $makefile;
+
+ ok( unlink($makefile_pl), "Deleting Makefile.PL");
+ ok( !-s $makefile_pl, " Makefile.PL deleted" );
+ ok( !-s $makefile, " Makefile deleted" );
+ ok($dist->write_makefile_pl," Makefile.PL written" );
+
+ ### see if we wrote anything sensible
+ my $fh = OPEN_FILE->( $makefile_pl );
+ ok( $fh, "Makefile.PL open for read" );
+
+ my $str = do { local $/; <$fh> };
+ like( $str, qr/### Auto-generated .+ by CPANPLUS ###/,
+ " Autogeneration noted" );
+ like( $str, '/'. $Mod->module .'/',
+ " Contains module name" );
+ like( $str, '/'. quotemeta($Mod->version) . '/',
+ " Contains version" );
+ like( $str, '/'. $Mod->author->author .'/',
+ " Contains author" );
+ like( $str, '/PREREQ_PM/', " Contains prereqs" );
+ like( $str, qr/$prereq.+0/, " Contains prereqs" );
+
+ close $fh;
+
+ ### seems ok, now delete it again and go via install()
+ ### to see if it picks up on the missing makefile.pl and
+ ### does the right thing
+ ok( unlink($makefile_pl), "Deleting Makefile.PL");
+ ok( !-s $makefile_pl, " Makefile.PL deleted" );
+ ok( $dist->status->mk_flush,"Dist status flushed" );
+ ok( $dist->prepare, " Dist->prepare run again" );
+ ok( $dist->create, " Dist->create run again" );
+ ok( -s $makefile_pl, " Makefile.PL present" );
+ like( CPANPLUS::Error->stack_as_string,
+ qr/attempting to generate one/,
+ " Makefile.PL generation attempt logged" );
+
+ ### now let's throw away the makefile.pl, flush the status and not
+ ### write a makefile.pl
+ { local $^W;
+ local *CPANPLUS::Dist::MM::write_makefile_pl = sub { 1 };
+
+ unlink $makefile_pl;
+ unlink $makefile;
+
+ ok(!-s $makefile_pl, "Makefile.PL deleted" );
+ ok(!-s $makefile, "Makefile deleted" );
+ ok( $dist->status->mk_flush,"Dist status flushed" );
+ ok(!$dist->prepare, " Dist->prepare failed" );
+ like( CPANPLUS::Error->stack_as_string,
+ qr/Could not find 'Makefile.PL'/i,
+ " Missing Makefile.PL noted" );
+ is( $dist->status->makefile, 0,
+ " Did not manage to create Makefile" );
+ }
+
+ ### now let's write a makefile.pl that just does 'die'
+ { local $^W;
+ local *CPANPLUS::Dist::MM::write_makefile_pl =
+ __PACKAGE__->_custom_makefile_pl_sub( "exit 1;" );
+
+ ### there's no makefile.pl now, since the previous test failed
+ ### to create one
+ #ok( -e $makefile_pl, "Makefile.PL exists" );
+ #ok( unlink($makefile_pl), " Deleting Makefile.PL");
+ ok(!-s $makefile_pl, "Makefile.PL deleted" );
+ ok( $dist->status->mk_flush,"Dist status flushed" );
+ ok(!$dist->prepare, " Dist->prepare failed" );
+ like( CPANPLUS::Error->stack_as_string, qr/Could not run/s,
+ " Logged failed 'perl Makefile.PL'" );
+ is( $dist->status->makefile, 0,
+ " Did not manage to create Makefile" );
+ }
+
+ ### clean up afterwards ###
+ ok( unlink($makefile_pl), "Deleting Makefile.PL");
+ $dist->status->mk_flush;
+
+}
+
+### test ENV setting in Makefile.PL
+{ ### use print() not die() -- we're redirecting STDERR in tests!
+ my $env = ENV_CPANPLUS_IS_EXECUTING;
+ my $sub = __PACKAGE__->_custom_makefile_pl_sub(
+ "print qq[ENV=\$ENV{$env}\n]; exit 1;" );
+
+ my $clone = $Mod->clone;
+ $clone->status->fetch( $Mod->status->fetch );
+
+ ok( $clone, 'Testing ENV settings $dist->prepare' );
+ ok( $clone->extract, ' Files extracted' );
+ ok( $clone->prepare, ' $mod->prepare worked first time' );
+
+ my $dist = $clone->status->dist;
+ my $makefile_pl = MAKEFILE_PL->( $clone->status->extract );
+
+ ok( $sub->($dist), " Custom Makefile.PL written" );
+ ok( -e $makefile_pl, " File exists" );
+
+ ### clear errors
+ CPANPLUS::Error->flush;
+
+ my $rv = $dist->prepare( force => 1, verbose => 0 );
+ ok( !$rv, ' $dist->prepare failed' );
+
+ SKIP: {
+ skip( "Can't test ENV{$env} -- no buffers available", 1 )
+ unless IPC::Cmd->can_capture_buffer;
+
+ my $re = quotemeta( $makefile_pl );
+ like( CPANPLUS::Error->stack_as_string, qr/ENV=$re/,
+ " \$ENV $env set correctly during execution");
+ }
+
+ ### and the ENV var should no longer be set now
+ ok( !$ENV{$env}, " ENV var now unset" );
+}
+
+sub _custom_makefile_pl_sub {
+ my $pkg = shift;
+ my $txt = shift or return;
+
+ return sub {
+ my $dist = shift;
+ my $self = $dist->parent;
+ my $fh = OPEN_FILE->(
+ MAKEFILE_PL->($self->status->extract), '>' );
+ print $fh $txt;
+ close $fh;
+
+ return 1;
+ }
+}
+
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
+
+
diff --git a/lib/CPANPLUS/t/21_CPANPLUS-Dist-No-Build.t b/lib/CPANPLUS/t/21_CPANPLUS-Dist-No-Build.t
new file mode 100644
index 0000000000..c4d1b5a00b
--- /dev/null
+++ b/lib/CPANPLUS/t/21_CPANPLUS-Dist-No-Build.t
@@ -0,0 +1,57 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use Test::More 'no_plan';
+
+use CPANPLUS::Dist;
+use CPANPLUS::Backend;
+use CPANPLUS::Module::Fake;
+use CPANPLUS::Module::Author::Fake;
+use CPANPLUS::Internals::Constants;
+
+my $Conf = gimme_conf();
+my $CB = CPANPLUS::Backend->new( $Conf );
+
+### set the config so that we will ignore the build installer,
+### but prefer it anyway
+{ CPANPLUS::Dist->_ignore_dist_types( INSTALLER_BUILD );
+ $Conf->set_conf( prefer_makefile => 0 );
+}
+
+my $Mod = $CB->module_tree( 'Foo::Bar::MB::NOXS' );
+
+ok( $Mod, "Module object retrieved" );
+ok( not grep { $_ eq INSTALLER_BUILD } CPANPLUS::Dist->dist_types,
+ " Build installer not returned" );
+
+### fetch the file first
+{ my $where = $Mod->fetch;
+ ok( -e $where, " Tarball '$where' exists" );
+}
+
+### extract it, silence warnings/messages
+{ local $CPANPLUS::Error::MSG_FH = output_handle();
+ local $CPANPLUS::Error::ERROR_FH = output_handle();
+
+ my $where = $Mod->extract;
+ ok( -e $where, " Tarball extracted to '$where'" );
+}
+
+### check the installer type
+{ is( $Mod->status->installer_type, INSTALLER_MM,
+ "Proper installer type found" );
+
+ my $err = CPANPLUS::Error->stack_as_string;
+ like( $err, '/'.INSTALLER_MM.'/',
+ " Error mentions " . INSTALLER_MM );
+ like( $err, '/'.INSTALLER_BUILD.'/',
+ " Error mentions " . INSTALLER_BUILD );
+ like( $err, qr/but might not be able to install/,
+ " Error mentions install warning" );
+}
+
+END { 1 while unlink output_file() }
diff --git a/lib/CPANPLUS/t/30_CPANPLUS-Internals-Selfupdate.t b/lib/CPANPLUS/t/30_CPANPLUS-Internals-Selfupdate.t
new file mode 100644
index 0000000000..79df1dfdad
--- /dev/null
+++ b/lib/CPANPLUS/t/30_CPANPLUS-Internals-Selfupdate.t
@@ -0,0 +1,122 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+
+use CPANPLUS::Backend;
+use Test::More 'no_plan';
+use Data::Dumper;
+
+my $conf = gimme_conf();
+$conf->set_conf( verbose => 0 );
+
+my $Class = 'CPANPLUS::Selfupdate';
+my $ModClass = "CPANPLUS::Selfupdate::Module";
+my $CB = CPANPLUS::Backend->new( $conf );
+my $Acc = 'selfupdate_object';
+my $Conf = $Class->_get_config;
+my $Dep = TEST_CONF_PREREQ; # has to be in our package file && core!
+my $Feat = 'some_feature';
+my $Prereq = { $Dep => 0 };
+
+### test the object
+{ ok( $CB, "New backend object created" );
+ can_ok( $CB, $Acc );
+
+ ok( $Conf, "Got configuration hash" );
+
+ my $su = $CB->$Acc;
+ ok( $su, "Selfupdate object retrieved" );
+ isa_ok( $su, $Class );
+}
+
+### test the feature list
+{ ### start with defining our OWN type of config, as not all mentioned
+ ### modules will be present in our bundled package files.
+ ### XXX WHITEBOX TEST!!!!
+ { delete $Conf->{$_} for keys %$Conf;
+ $Conf->{'dependencies'} = $Prereq;
+ $Conf->{'core'} = $Prereq;
+ $Conf->{'features'}->{$Feat} = [ $Prereq, sub { 1 } ];
+ }
+
+ is_deeply( $Conf, $Class->_get_config,
+ "Config updated succesfully" );
+
+ my @feat = $CB->$Acc->list_features;
+ ok( scalar(@feat), "Features list returned" );
+
+ ### test if we get modules for each feature
+ for my $feat (@feat) {
+ my $meth = 'modules_for_feature';
+ my @mods = $CB->$Acc->$meth( $feat );
+
+ ok( $feat, "Testing feature '$feat'" );
+ ok( scalar( @mods ), " Module list returned" );
+
+ my $acc = 'is_installed_version_sufficient';
+ for my $mod (@mods) {
+ isa_ok( $mod, "CPANPLUS::Module" );
+ isa_ok( $mod, $ModClass );
+ can_ok( $mod, $acc );
+ ok( $mod->$acc, " Module uptodate" );
+ }
+
+ ### check if we can get a hashref
+ { my $href = $CB->$Acc->$meth( $feat, 1 );
+ ok( $href, "Got result as hash" );
+ isa_ok( $href, 'HASH' );
+ is_deeply( $href, $Prereq,
+ " With the proper entries" );
+
+ }
+
+ }
+
+ ### find enabled features
+ { my $meth = 'list_enabled_features';
+ can_ok( $Class, $meth );
+
+ my @list = $CB->$Acc->$meth;
+ ok( scalar(@list), "Retrieved enabled features" );
+ is_deeply( [$Feat], \@list,
+ " Proper features found" );
+ }
+
+ ### find dependencies/core modules
+ for my $meth ( qw[list_core_dependencies list_core_modules] ) {
+ can_ok( $Class, $meth );
+
+ my @list = $CB->$Acc->$meth;
+ ok( scalar(@list), "Retrieved modules" );
+ is( scalar(@list), 1, " 1 Found" );
+ isa_ok( $list[0], $ModClass );
+ is( $list[0]->name, $Dep,
+ " Correct module found" );
+
+ ### check if we can get a hashref
+ { my $href = $CB->$Acc->$meth( 1 );
+ ok( $href, "Got result as hash" );
+ isa_ok( $href, 'HASH' );
+ is_deeply( $href, $Prereq,
+ " With the proper entries" );
+ }
+ }
+
+ ### now selfupdate ourselves
+ { ### XXX just test the mechanics, make sure install returns true
+ ### declare twice because warnings are hateful
+ ### declare in a block to quelch 'sub redefined' warnings.
+ { local *CPANPLUS::Selfupdate::Module::install = sub { 1 }; }
+ local *CPANPLUS::Selfupdate::Module::install = sub { 1 };
+
+ my $meth = 'selfupdate';
+ can_ok( $Class, $meth );
+ ok( $CB->$Acc->$meth( update => 'all'),
+ " Selfupdate successful" );
+ }
+}
+
diff --git a/lib/CPANPLUS/t/40_CPANPLUS-Internals-Report.t b/lib/CPANPLUS/t/40_CPANPLUS-Internals-Report.t
new file mode 100644
index 0000000000..b028404085
--- /dev/null
+++ b/lib/CPANPLUS/t/40_CPANPLUS-Internals-Report.t
@@ -0,0 +1,469 @@
+### make sure we can find our conf.pl file
+BEGIN {
+ use FindBin;
+ require "$FindBin::Bin/inc/conf.pl";
+}
+
+use strict;
+use CPANPLUS::Backend;
+use CPANPLUS::Internals::Constants::Report;
+
+my $send_tests = 55;
+my $query_tests = 8;
+my $total_tests = $send_tests + $query_tests;
+
+use Test::More 'no_plan';
+use Module::Load::Conditional qw[can_load];
+
+use FileHandle;
+use Data::Dumper;
+
+use constant NOBODY => 'nobody@xs4all.nl';
+
+my $conf = gimme_conf();
+my $CB = CPANPLUS::Backend->new( $conf );
+my $ModName = TEST_CONF_MODULE;
+my $ModPrereq = TEST_CONF_PREREQ;
+my $Mod = $CB->module_tree($ModName);
+my $int_ver = $CPANPLUS::Internals::VERSION;
+
+### explicitly enable testing if possible ###
+$CB->configure_object->set_conf(cpantest =>1) if $ARGV[0];
+
+my $map = {
+ all_ok => {
+ buffer => '', # output from build process
+ failed => 0, # indicate failure
+ match => [qw|/PASS/|], # list of regexes for the output
+ check => 0, # check if callbacks got called?
+ },
+ skipped_test => {
+ buffer => '',
+ failed => 0,
+ match => ['/PASS/',
+ '/tests for this module were skipped during this build/',
+ ],
+ check => 0,
+ skiptests
+ => 1, # did we skip the tests?
+ },
+ missing_prereq => {
+ buffer => missing_prereq_buffer(),
+ failed => 1,
+ match => ['/The comments above are created mechanically/',
+ '/computer-generated error report/',
+ '/Below is the error stack from stage/',
+ '/test suite seem to fail without these modules/',
+ '/floo/',
+ '/FAIL/',
+ '/make test/',
+ ],
+ check => 1,
+ },
+ missing_tests => {
+ buffer => missing_tests_buffer(),
+ failed => 1,
+ match => ['/The comments above are created mechanically/',
+ '/computer-generated error report/',
+ '/Below is the error stack from stage/',
+ '/RECOMMENDATIONS/',
+ '/UNKNOWN/',
+ '/make test/',
+ ],
+ check => 0,
+ },
+ perl_version_too_low_mm => {
+ buffer => perl_version_too_low_buffer_mm(),
+ failed => 1,
+ match => ['/This distribution has been tested/',
+ '/http://testers.cpan.org/',
+ '/NA/',
+ ],
+ check => 0,
+ },
+ perl_version_too_low_build1 => {
+ buffer => perl_version_too_low_buffer_build(1),
+ failed => 1,
+ match => ['/This distribution has been tested/',
+ '/http://testers.cpan.org/',
+ '/NA/',
+ ],
+ check => 0,
+ },
+ perl_version_too_low_build2 => {
+ buffer => perl_version_too_low_buffer_build(2),
+ failed => 1,
+ match => ['/This distribution has been tested/',
+ '/http://testers.cpan.org/',
+ '/NA/',
+ ],
+ check => 0,
+ },
+ prereq_versions_too_low => {
+ ### set the prereq version incredibly high
+ pre_hook => sub {
+ my $mod = shift;
+ my $clone = $mod->clone;
+ $clone->status->prereqs( { $ModPrereq => ~0 } );
+ return $clone;
+ },
+ failed => 1,
+ match => ['/This distribution has been tested/',
+ '/http://testers.cpan.org/',
+ '/NA/',
+ ],
+ check => 0,
+ },
+ prereq_not_on_cpan => {
+ pre_hook => sub {
+ my $mod = shift;
+ my $clone = $mod->clone;
+ $clone->status->prereqs(
+ { TEST_CONF_INVALID_MODULE, 0 }
+ );
+ return $clone;
+ },
+ failed => 1,
+ match => ['/This distribution has been tested/',
+ '/http://testers.cpan.org/',
+ '/NA/',
+ ],
+ check => 0,
+ },
+
+
+
+};
+
+### test config settings
+{ for my $opt ( qw[cpantest cpantest_mx] ) {
+ my $warnings;
+ local $SIG{__WARN__} = sub { $warnings .= "@_" };
+
+ my $org = $conf->get_conf( $opt );
+ ok( $conf->set_conf( $opt => $$ ),
+ "Setting option $opt to $$" );
+ is( $conf->get_conf( $opt ), $$,
+ " Retrieved properly" );
+ ok( $conf->set_conf( $opt => $org ),
+ " Option $opt set back to original" );
+ ok( !$warnings, " No warnings" );
+ }
+}
+
+### test constants ###
+{ { my $to = CPAN_MAIL_ACCOUNT->('foo');
+ is( $to, 'foo@cpan.org', "Got proper mail account" );
+ }
+
+ { ok(RELEVANT_TEST_RESULT->($Mod),"Test is relevant" );
+
+ ### test non-relevant tests ###
+ my $cp = $Mod->clone;
+ $cp->module( $Mod->module . '::' . ($^O eq 'beos' ? 'MSDOS' : 'Be') );
+ ok(!RELEVANT_TEST_RESULT->($cp),"Test is irrelevant");
+ }
+
+ { my $support = "it works!";
+ my @support = ( "No support for OS",
+ "OS unsupported",
+ "os unsupported",
+ );
+ ok(!UNSUPPORTED_OS->($support), "OS supported");
+ ok( UNSUPPORTED_OS->($_), "OS not supported") for(@support);
+ }
+
+ { ok(PERL_VERSION_TOO_LOW->( perl_version_too_low_buffer_mm() ),
+ "Perl version too low" );
+ ok(PERL_VERSION_TOO_LOW->( perl_version_too_low_buffer_build(1) ),
+ "Perl version too low" );
+ ok(PERL_VERSION_TOO_LOW->( perl_version_too_low_buffer_build(2) ),
+ "Perl version too low" );
+ ok(!PERL_VERSION_TOO_LOW->('foo'),
+ " Perl version adequate" );
+ }
+
+ { my $tests = "test.pl";
+ my @none = ( "No tests defined for Foo extension.",
+ "'No tests defined for Foo::Bar extension.'",
+ "'No tests defined.'",
+ );
+ ok(!NO_TESTS_DEFINED->($tests), "Tests defined");
+ ok( NO_TESTS_DEFINED->($_), "No tests defined") for(@none);
+ }
+
+ { my $fail = 'MAKE TEST'; my $unknown = 'foo';
+ is( TEST_FAIL_STAGE->($fail), lc $fail,
+ "Proper test fail stage found" );
+ is( TEST_FAIL_STAGE->($unknown), 'fetch',
+ "Proper test fail stage found" );
+ }
+
+ ### test missing prereqs
+ { my $str = q[Can't locate Foo/Bar.pm in @INC];
+
+ ### standard test
+ { my @list = MISSING_PREREQS_LIST->( $str );
+ is( scalar(@list), 1, " List of missing prereqs found" );
+ is( $list[0], 'Foo::Bar', " Proper prereq found" );
+ }
+
+ ### multiple mentions of same prereq
+ { my @list = MISSING_PREREQS_LIST->( $str . $str );
+
+ is( scalar(@list), 1, " 1 result for multiple mentions" );
+ is( $list[0], 'Foo::Bar', " Proper prereq found" );
+ }
+ }
+
+ { # cp version, author
+ my $header = REPORT_MESSAGE_HEADER->($int_ver,'foo');
+ ok( $header, "Test header generated" );
+ like( $header, qr/Dear foo,/, " Proper content found" );
+ like( $header, qr/puter-gen/, " Proper content found" );
+ like( $header, qr/CPANPLUS,/, " Proper content found" );
+ like( $header, qr/ments may/, " Proper content found" );
+ }
+
+ { # stage, buffer
+ my $header = REPORT_MESSAGE_FAIL_HEADER->('test','buffer');
+ ok( $header, "Test header generated" );
+ like( $header, qr/uploading/, " Proper content found" );
+ like( $header, qr/RESULTS:/, " Proper content found" );
+ like( $header, qr/stack/, " Proper content found" );
+ like( $header, qr/buffer/, " Proper content found" );
+ }
+
+ { my $prereqs = REPORT_MISSING_PREREQS->('foo','bar@example.com','Foo::Bar');
+ ok( $prereqs, "Test output generated" );
+ like( $prereqs, qr/'foo \(bar\@example\.com\)'/,
+ " Proper content found" );
+ like( $prereqs, qr/Foo::Bar/, " Proper content found" );
+ like( $prereqs, qr/prerequisi/, " Proper content found" );
+ like( $prereqs, qr/PREREQ_PM/, " Proper content found" );
+ }
+
+ { my $prereqs = REPORT_MISSING_PREREQS->(undef,undef,'Foo::Bar');
+ ok( $prereqs, "Test output generated" );
+ like( $prereqs, qr/Your Name/, " Proper content found" );
+ like( $prereqs, qr/Foo::Bar/, " Proper content found" );
+ like( $prereqs, qr/prerequisi/, " Proper content found" );
+ like( $prereqs, qr/PREREQ_PM/, " Proper content found" );
+ }
+
+ { my $missing = REPORT_MISSING_TESTS->();
+ ok( $missing, "Missing test string generated" );
+ like( $missing, qr/tests/, " Proper content found" );
+ like( $missing, qr/Test::More/, " Proper content found" );
+ }
+
+ { my $missing = REPORT_MESSAGE_FOOTER->();
+ ok( $missing, "Message footer string generated" );
+ like( $missing, qr/NOTE/, " Proper content found" );
+ like( $missing, qr/identical/, " Proper content found" );
+ like( $missing, qr/mistaken/, " Proper content found" );
+ like( $missing, qr/appreciate/, " Proper content found" );
+ like( $missing, qr/Additional/, " Proper content found" );
+ }
+
+ { my @libs = MISSING_EXTLIBS_LIST->("No library found for -lfoo\nNo library found for -lbar");
+ ok( @libs, "Missing external libraries found" );
+ my @list = qw(foo bar);
+ is_deeply( \@libs, \@list, " Proper content found" );
+ }
+
+ { my $clone = $Mod->clone;
+ my $prereqs = { $ModPrereq => ~0 };
+
+ $clone->status->prereqs( $prereqs );
+
+ my $str = REPORT_LOADED_PREREQS->( $clone );
+
+ like($str, qr/PREREQUISITES:/, "Listed loaded prerequisites" );
+ like($str, qr/\! $ModPrereq\s+\S+\s+\S+/,
+ " Proper content found" );
+ }
+}
+
+### callback tests
+{ ### as reported in bug 13086, this callback returned the wrong item
+ ### from the list:
+ ### $self->_callbacks->munge_test_report->($Mod, $message, $grade);
+ my $rv = $CB->_callbacks->munge_test_report->( 1..4 );
+ is( $rv, 2, "Default 'munge_test_report' callback OK" );
+}
+
+
+### test creating test reports ###
+SKIP: {
+ skip "You have chosen not to enable test reporting", $total_tests,
+ unless $CB->configure_object->get_conf('cpantest');
+
+ skip "No report send & query modules installed", $total_tests
+ unless $CB->_have_query_report_modules(verbose => 0);
+
+
+ SKIP: {
+ my $mod = $CB->module_tree( TEST_CONF_PREREQ ); # is released to CPAN
+ ok( $mod, "Module retrieved" );
+
+ ### so we're not pinned down to this specific version of perl
+ my @list = $mod->fetch_report( all_versions => 1 );
+ skip "Possibly no net connection, or server down", 7 unless @list;
+
+ my $href = $list[0];
+ ok( scalar(@list), "Fetched test report" );
+ is( ref $href, ref {}, " Return value has hashrefs" );
+
+ ok( $href->{grade}, " Has a grade" );
+
+ ### XXX use constants for grades?
+ like( $href->{grade}, qr/pass|fail|unknown|na/i,
+ " Grade as expected" );
+
+ my $pkg_name = $mod->package_name;
+ ok( $href->{dist}, " Has a dist" );
+ like( $href->{dist}, qr/$pkg_name/, " Dist as expected" );
+
+ ok( $href->{platform}, " Has a platform" );
+ }
+
+ skip "No report sending modules installed", $send_tests
+ unless $CB->_have_send_report_modules(verbose => 0);
+
+ for my $type ( keys %$map ) {
+
+
+ ### never enter the editor for test reports
+ ### but check if the callback actually gets called;
+ my $called_edit; my $called_send;
+ $CB->_register_callback(
+ name => 'edit_test_report',
+ code => sub { $called_edit++; 0 }
+ );
+
+ $CB->_register_callback(
+ name => 'send_test_report',
+ code => sub { $called_send++; 1 }
+ );
+
+ ### reset from earlier tests
+ $CB->_register_callback(
+ name => 'munge_test_report',
+ code => sub { return $_[1] }
+ );
+
+ my $mod = $map->{$type}->{'pre_hook'}
+ ? $map->{$type}->{'pre_hook'}->( $Mod )
+ : $Mod;
+
+ my $file = $CB->_send_report(
+ module => $mod,
+ buffer => $map->{$type}{'buffer'},
+ failed => $map->{$type}{'failed'},
+ tests_skipped => ($map->{$type}{'skiptests'} ? 1 : 0),
+ save => 1,
+ dontcc => 1, # no need to send, and also skips
+ # fetching reports from testers.cpan
+ );
+
+ ok( $file, "Type '$type' written to file" );
+ ok( -e $file, " File exists" );
+
+ my $fh = FileHandle->new($file);
+ ok( $fh, " Opened file for reading" );
+
+ my $in = do { local $/; <$fh> };
+ ok( $in, " File has contents" );
+
+ for my $regex ( @{$map->{$type}->{match}} ) {
+ like( $in, $regex, " File contains expected contents" );
+ }
+
+ ### check if our registered callback got called ###
+ if( $map->{$type}->{check} ) {
+ ok( $called_edit, " Callback to edit was called" );
+ ok( $called_send, " Callback to send was called" );
+ }
+
+ #unlink $file;
+
+
+### T::R tests don't even try to mail, let's not try and be smarter
+### ourselves
+# { ### use a dummy 'editor' and see if the editor
+# ### invocation doesn't break things
+# $conf->set_program( editor => "$^X -le1" );
+# $CB->_callbacks->edit_test_report( sub { 1 } );
+#
+# ### XXX whitebox test!!! Might change =/
+# ### this makes test::reporter not ask for what editor to use
+# ### XXX stupid lousy perl warnings;
+# local $Test::Reporter::MacApp = 1;
+# local $Test::Reporter::MacApp = 1;
+#
+# ### now try and mail the report to a /dev/null'd mailbox
+# my $ok = $CB->_send_report(
+# module => $Mod,
+# buffer => $map->{$type}->{'buffer'},
+# failed => $map->{$type}->{'failed'},
+# address => NOBODY,
+# dontcc => 1,
+# );
+# ok( $ok, " Mailed report to NOBODY" );
+# }
+ }
+}
+
+
+sub missing_prereq_buffer {
+ return q[
+MAKE TEST:
+Can't locate floo.pm in @INC (@INC contains: /Users/kane/sources/p4/other/archive-extract/lib /Users/kane/sources/p4/other/file-fetch/lib /Users/kane/sources/p4/other/archive-tar-new/lib /Users/kane/sources/p4/other/carp-trace/lib /Users/kane/sources/p4/other/log-message/lib /Users/kane/sources/p4/other/module-load/lib /Users/kane/sources/p4/other/params-check/lib /Users/kane/sources/p4/other/qmail-checkpassword/lib /Users/kane/sources/p4/other/module-load-conditional/lib /Users/kane/sources/p4/other/term-ui/lib /Users/kane/sources/p4/other/ipc-cmd/lib /Users/kane/sources/p4/other/config-auto/lib /Users/kane/sources/NSA /Users/kane/sources/NSA/misc /Users/kane/sources/NSA/test /Users/kane/sources/beheer/perl /opt/lib/perl5/5.8.3/darwin-2level /opt/lib/perl5/5.8.3 /opt/lib/perl5/site_perl/5.8.3/darwin-2level /opt/lib/perl5/site_perl/5.8.3 /opt/lib/perl5/site_perl .).
+BEGIN failed--compilation aborted.
+ ];
+}
+
+sub missing_tests_buffer {
+ return q[
+cp lib/Acme/POE/Knee.pm blib/lib/Acme/POE/Knee.pm
+cp demo_race.pl blib/lib/Acme/POE/demo_race.pl
+cp demo_simple.pl blib/lib/Acme/POE/demo_simple.pl
+MAKE TEST:
+No tests defined for Acme::POE::Knee extension.
+ ];
+}
+
+sub perl_version_too_low_buffer_mm {
+ return q[
+Running [/usr/bin/perl5.8.1 Makefile.PL ]...
+Perl v5.8.3 required--this is only v5.8.1, stopped at Makefile.PL line 1.
+BEGIN failed--compilation aborted at Makefile.PL line 1.
+[ERROR] Could not run '/usr/bin/perl5.8.1 Makefile.PL': Perl v5.8.3 required--this is only v5.8.1, stopped at Makefile.PL line 1.
+BEGIN failed--compilation aborted at Makefile.PL line 1.
+ -- cannot continue
+ ];
+}
+
+sub perl_version_too_low_buffer_build {
+ my $type = shift;
+ return q[
+ERROR: perl: Version 5.006001 is installed, but we need version >= 5.008001
+ERROR: version: Prerequisite version isn't installed
+ERRORS/WARNINGS FOUND IN PREREQUISITES. You may wish to install the versions
+ of the modules indicated above before proceeding with this installation.
+ ] if($type == 1);
+ return q[
+ERROR: Version 5.006001 of perl is installed, but we need version >= 5.008001
+ERROR: version: Prerequisite version isn't installed
+ERRORS/WARNINGS FOUND IN PREREQUISITES. You may wish to install the versions
+ of the modules indicated above before proceeding with this installation.
+ ] if($type == 2);
+}
+
+# Local variables:
+# c-indentation-style: bsd
+# c-basic-offset: 4
+# indent-tabs-mode: nil
+# End:
+# vim: expandtab shiftwidth=4:
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz.packed
new file mode 100644
index 0000000000..7606c3b7e5
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz.packed
@@ -0,0 +1,19 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz.packed lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz lib/CPANPLUS/t/dummy-CPAN/authors/01mailrc.txt.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("%_EO$4``S`Q;6%I;')C+G1X=`!+S,E,+%9P#8T(5@`#)=>*DM"2S)QB
+M*RO?Q.Q4$"Y2`$HZYZ>D*M@D%R3F%>24%CND5B3F%N2DZB7GY]HI<25"#?'S
+MAQB#U1"_?"+-\76".\8W/Z4T)]7*RJDT,R>%:.UP9Z!J]_,G:`(`W)]=R`X!
+"````
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..20d4cb2fda
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,34 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("!1%OT4"`T)U;F1L92U&;V\M0F%R+3`N,#$N=&%R`.V7:V_:,!2&^8I_
+MQ6F91"L52"`)6KI6`Q8TI`$5EZW2-"%##%C-K8FSEDW[[W,2:&E$6VGKH$Q^
+M(`KR<4YB7K_'3CUT3(L4FJY;J&._(!4EN91Y821.5563LY:<.:MS_%N65+6J
+M*67^R4BR(E<J&5`S6R`,&/8!,E?8(4_UNYD38CTSR+5!9?:$^@;]+3HN[5C_
+MJB0+_7>I?])>VI'^FBR7A?ZO0'_>7MJ%_EI%%?J_$OUY>]&S_U)_35$>U5^5
+MM'O]I2K7OQK7?TGH_\_Q\.0*SP@D>NLZ%US7N>*G"+WY;/3ZK6X'SN`PFA>'
+MO$WFQVAD=#Z,1@B=S0DV9>C4V@9"Z01P%V]T.P.C,^@CM(KINC'4]<L^<*+,
+MZ4BG&\72D7;]L6NBR/HUC1L3TB21Y2/5AH./W1Y"QBT;,FH%/`6^(M'A0\<%
+MGJGAF@3>33SL>%88O">WV/8L4IRX]CG/,@D9']_]/Y'96S;Y/_H?II2/]N+3
+MBZW_3_I?45?^E\OE<KS^2XKP_W:&3V"#"TY1%`B83R>,6_Z+3QE930LX0I&A
+M(MNO&^SL'/+I(I`_B;LN"\FHV>NV5UTW+C+Y$\C!E#IF`*OJ$R<P:<!2]_K)
+MZTK[HF?T^W&ZV0_J0>'ME&?H#YO-UN6R-0^_DF=(+/_P<?_(_GQ,V5J]/^C5
+M&H.U9-=?TX/_=H*.3Z,HVC?_USJMIM$?O.3[WU/^+\O5U/Y/+6O"_UMAHPW1
+MV@J`5K,AF\T.YC0`*_*B.X4H'J"V,:@5%[8%S]%VS9#7#ILP7#`QPW"$39.8
+M,%[`G?F.]W@A_9_6_Z6FV_*_4DV__ZF*I`K_;X,<S!GS]%+)COU9&(?4,HN!
+M&_H3,G7]&2DZA,4SHA!X9%*<,]M"N<L$B`L"_V+P?)>Y;.&1@X,#@!:#&VI9
+M,)ECA[]<4`?8G,`T9*$?=XBOSB$'VT2_JQ`/IR+Z3OR`NHZ^MGU?-HVFOFOK
+ML+ET48<+:EDF]0-^:<#W+<@GUR'U2:`C%&TD?#H.690F>EP=DH&C&7&(CQDQ
+H1^.%OFE+!,N[@U:L2*)0"00"@4`@$`@$`H%`(-@;?@-?EF['`"@`````
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/CHECKSUMS
new file mode 100644
index 0000000000..e716d3635c
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/CHECKSUMS
@@ -0,0 +1,30 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '2917421f5a41419f7bb2d2cf87f04b8d',
+ 'size' => 1066
+ },
+ 'perl5.005_03.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '2b70961796a2ed7ca21fbf7e0c615643',
+ 'size' => 119
+ },
+ 'Bundle-Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '76f9c0eed0de9f533ed4d3922bac2f11',
+ 'size' => 850
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.readme b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.readme
new file mode 100644
index 0000000000..ba8894c152
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.readme
@@ -0,0 +1,2 @@
+README
+
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..c58d0e12eb
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,39 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("/8X34("`T9O;RU"87(M,"XP,2YT87(`[9KQ;]I&%,?Y^?Z*1YE$(A5C
+M8YM(SE*-K69!*LD4Z%:IJZ(#'V!A^[SSN2F:]K_O#ML,6)ONAP+M\CY*XMCW
+MS/EX]WWWGNT^YZT?J6B9AFFU:X?!5%RXKMY:%ZZUO:VH66:G8W=LQ^YV:Z;5
+M<1VS!F[M".29I`*@MJ0)>\SN<^W50*KM-T)_V_\_7?=N?O9'A_"_XSB?]G_'
+M*?VO)L"%]K_M=MP:F.C_@S,`&@.%TO,P"R-&:LB384?_P][-H.^/Q@?0?_<1
+M_7?L_?BO_CJH_V-0"I_<^;V70Y\,Z9+I&&#\\HI4LX%(EDDCC4@43MIJOK35
+M?#'2F$S"I#WC?*+W(C+TQSUC%4?P.88\R",&,9.T%5!)X8P&`0M@L@+=N_X5
+MYQB#3J+_TH='UK_C=O;T[Z@4$/5_#!JPD#+UVNUX+<O6)`^CP,AX+J9LQL6<
+M&0F3ZXG1RE(V-18RCDCC30&,%V$&ZH="*KCD<I6R>KT.,)#P$$813!<TF3,(
+M$Y`+!K-<YF)ML#Z[01(:,V\3&,JI2-XSD84\*1OTQ*P.W<\$CSW8BT-AHEP8
+M14$H,G5.%DI&!/LC#P7+/$*",),BG.12GZ\OT(-BJ&3.$B:H9,']9.6!_T&^
+MEF&4>=XF"D'9+72-CDN>@/[+0'_T_+]K[>7_CNE<H/Z/HO]Z.\]$6Z_E*1,1
+M(:D($PG/+,.P?D^>75;[?`G%/BZ9_U_]Z\#Z5=S_L6VSB_=_3N)_O;">W/^.
+M:5OH_Y/YOTBLCK7^:]?OK?^NB?7_<2!7"T8#"VYZJOHG(QJGJC9OZ5P<5&T/
+M67%`_4LJR][K\?7MW<9V2!/XOC#[@7U8;XTICU]L[&_'U[XRKW+I*$Q8IBN"
+ME`>0+7@>J=)?E0CSA`L6>.2[7_V[T>#V!JZ@F7`)?.G!7&VK\W4)H,^M-R]5
+M%]-<JI2%3I=4E1GJHCU/S5W5T(!_]:<N*F:)S#[6:0/^4[?E1ZR[;JB2IBA^
+M=&FSW1L\L*9@$'&^#).Y^N[$SJC.FEIKS?.WYKM+@,;6Y;#W-&H&A/Q)]/$'
+M5C7Q)%J!5!5)446I.D?N=CCC>:)&`:K/*8V(+IL^-9Z,37D2[`\K8'I,?Q%B
+M88+WA/,_705\)?F?A<__3N/_S1W=+^M_]S'_.^[^^F]9^/SOI/5_,0U4O8]+
+MPM.)_UM/?[YT_'],_QW7WK__YUHVZO\XPV<?N_6M=/^;""6K9L39.JG41<+V
+MH[RK%]"LDN[F\[5)F7;>]^]NAY7);EE9&OIO_/O^X)4_^N>SWC9W%J#FN^>Z
+M@9Q?K@TP"AU<_\5#X(/D?X^__[.O?]NQ+-3_,2C?_RD\CZ__(`B"(`B"(`B"
+?(`B"(`B"(`B"(`B"(`B"(`B"(,@WP=^)5B"Y`%``````
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed
new file mode 100644
index 0000000000..cb93428215
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed
@@ -0,0 +1,18 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL(`'3DO44``^W/,0J`,`Q`T1RE)Y"T-O4XXN"DB%2]OR(*NNC4[;_E#\F0
+MS'T>K5*U5FLI1`\IQK--LE<OXM6LJ5/P,8CZ8$'%::F#GK9E[;)S,G13_[7W
+=-[\?N0L`````````````````0$$[-9`]0P`H````
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/CHECKSUMS
new file mode 100644
index 0000000000..f124759db0
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/CHECKSUMS
@@ -0,0 +1,20 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => 'c7691a12e5faa70b3a0e83402d279bd6',
+ 'size' => 1589
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.readme b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.readme
new file mode 100644
index 0000000000..ba8894c152
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.readme
@@ -0,0 +1,2 @@
+README
+
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..fd1640911e
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,51 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-CPAN/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("`DY34("`T9O;RU"87(M,"XP,2YT87(`[5IK3]M(%,U7YE=<H%5``A.;
+M/*0@5@0P-"IY*`E=JFX53>))8F%[4L\8B%#_^][Q(P^VI5\VZ78[1P0G]IWG
+MO>?,^-I7G!^>T_"P8!3,H]QZ4$!42B5U-"LE<_F8(6<6+.O8.BX633QO6B6K
+MDH-2;@.(A*0A0.Z>!NPUNQ]=SP:2'7\17"W[_^)=K7EM=]?A_V*Q^'W_6\74
+M_Q@`E3+Z_[ADE7)0T/Y?.^I`?:"0>AY&KL=(3N.WP0K_&[5F_<KN]C:L_Q:*
+M_:K^'Y?+1<W_32`E/NG8M<N&3;(((`UZSY08&.T;@O%A3/WX\"2(9$(:4X\,
+MW.!HQ/E`7?1(P^[5C)GOP8_0X$[D,?"9I(<.E13VJ.,P!P8S4&VJ3[BO)>CG
+M\#]UXAKX7WYE_2\6K27^%Q3_*Y6*YO\FL`L3*:?5HR,_YN7A('(]QQ`\"H=L
+MQ,,Q,P(FX\`X%%,V-";2]\CN70+H35P!^$=A&G+)Y6S*MK>W`>H2'EW/@^&$
+M!F,&;@!RPF`4R2B,#>+2NR2@/JO.E2$-1?+`0N'R(+V@`C,[U1^%W*]"JD=N
+M@*[S/,<-!=H*5S(2LB^1&S)1)<1QA0S=0215.=6Q*B1#)&,6L)!*YO0'LRK8
+M3_)6NIZH5N?R`VES4#:L$OF-^)]*^SKV_Z77[O\JA1?[_V*A9&G^;P+G]G6]
+M"<_(7S>0L&,:AOE7L`-?3T@DF*)DM8KA<4+2Z_P>U/43O4#_'_FOMG3_B?S/
+ML571^9^?X__YEGZ#^E\LO=#_DFGJ_,]F]G_;1Y$(8\]/6>B13.B3,$"A)Z86
+M^]]%_Y,[_+7H_VO\MY#S+_*_9:NL^;\1_KO!T(L<!COV7<_N-(W)#EF<4Y*P
+M>N:N>WNNSI!&Z_+VQH;3^1YQ:ZM=NWA?NUX]1_`V31)7]-D#"_;<8!K)?;*E
+M)$8A_DW4MXO6I5TE6QV[]Z%V@S4DIO`6+#@]A<+^26S5NNVU;WMS.RU,_S[_
+MI_ZF^6]9Q<H_^&\6-/\W@2D=WM/Q\IV>NN]3J9.AQ,4_3:B`_33EH61X.3MS
+M.0OH#:<.2XL\T%#`E\<].*MW:_#6OFNW.KU^KW;=A;/T1^M]]A7>?+`[W7JK
+M"4AL$I<X586S9I9J5P:[4)?,%R`YL-@"A0._#ZGG,6Q5I9$$#H2I)++#1C3R
+MI`%-+ED5'`X!EVDQK"BV7;*#1U=.."H-55F?&8PY=R!D5/#`@%L<UZ+O*M_$
+MJ&-@+9=)K<+UI]XLZQ/V!F8\"F$:#3QW"*,H&*KLDSCR&;;AB*,A5RFK0`I#
+MC2E.GF$A_BBP-T./AE29;ZG9O.OVF)"0K^+U_(F:@)&J.QM.P)@#$LL?@,\?
+MW&"L?@1CK`==,Y38IWB"LMGFX9(/L+(X-R?H@TK#^SR<&63%7ZB^D%<-P^D?
+M\$GYA6R1??B<.&LQ(<KN[!G>+)5]CLM]A:_+MHEO51TG9.[X4\@KS<'!D0'G
+M$B..3N=1.(^/V/GMD'DJ%AQ()Q*]!!,6,B/>G?;[=O.RWT?#<[1[5/E((:,!
+MSM4P\ED@XUF%$4Y"[)TD"VC`1YS.`9,JVICC8DC);4).)^AA$YJUADU(ZH1#
+M:.,RB%Z6+!!950./3A;_Y@6['YNM=K?>);A<+?RH%J_O%+BTNQ>=>KN'8R6D
+M^^UN)Y4<P!#C4B:/2B;6DS!4FM7C_%Z`Y]XSE6(E-,()"H&/XH3KHLN/%&G"
+MQIX[QIJ!!3P:3Q2=/*:B0-FJ*2-1H*:"88R3\Y7^&FF'K90/A#1YL,(W,A]2
+M[;;WKM4AI(8$,C#*>7@`U(@,U;,S:HRI1Y]FQ@C7&O6ACW2VF#[;AMI-MX6W
+M(3CE>^:^:G>(6P2]4&]B_5]ZZ+?1_7_Y^&7^MV3JY[^YS0R??>L1".KNGR$J
+M0181>_'V.Z]T,;]XEHO+0SZ3[/Q!8I(*=_^JTVKD$Y-D6YD_@%T8N0'*=Z;N
+M<0G[SNY?U6_L[J+23_F53%3^\T'6HEI"-&G7Q?_D)8"U[/]??__K)?^/51I8
+M\W\#2-__2CRO7__2T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0
+.T-#X9?`W%LHWQP!0````
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/CHECKSUMS
new file mode 100644
index 0000000000..042008cc56
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/CHECKSUMS
@@ -0,0 +1,20 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '1f52c2e83140814f734c8674e8fae53f',
+ 'size' => 867
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.readme b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.readme
new file mode 100644
index 0000000000..ba8894c152
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.readme
@@ -0,0 +1,2 @@
+README
+
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..11ada7ef22
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,35 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("-<X34(``T9O;RU"87(M,"XP,2YT87(`[9E;;]HP%,=Y]J<X+9722@-R
+M!0G6:G2#M=)*I]+MI:M0((9&)#;*I5TU[;O/)J1JH!WK%-)U/;\7XTOXVSD^
+M]G'<Y;QR:`<5M:IJM=)F4`4-RY*IUK"T^VE*25-UW=`-T]`;)573+<,J@;6A
+M_F2(P\@.`$I3F]'?M5M7GPXD35\(W?OV?W_4[GWL]//6D.^C;IJ/VU\WE^QO
+M6*(("GF)K]S^QV#[8,/"\C!V/4J>NT](<63\_Z3=.^YV^N<Y:ZSS?TVM+_M_
+MO6Z@_Q?!PO')6:?]X:1##F/7<ZJ?/Y%T*I"(AE%UYA'/'=;$9*F)R5*=^63H
+MLMJ8\Z',>;ABO%@R_K^P==X::_?_NK;D_Z9JUM'_BZ"\58O#H";=>48#CY!9
+MX+((MK5J5?O&MEMIGD\AR3]WAY%<R?B_7.,WH/'T\Y]AJ`:>_XI@Q?YRC\]9
+MX^GV-U6]CO8O@@?MG\1XN6FLV?^EZ9?L;ZD8_Q<#V;^BMJ-!KRVB?]*W_9E'
+MH0)B%H`([R%,"L1/DK9L?SD_.CV[:WMB,WB;-'M'O\_3ZHC[!W?M3\^/.J+Y
+M-0U"ES/P7$9#<!G,N`/A%8\]!X84W`GC`76:9.=KYZQ_?-J#?5`8CX!/FS`1
+M:?K\.."^?'9+:0F)41R)D,4>3>T)E9UN-L7<%15E6-$3G?(IB\*'1,OP1[*+
+MOYA+ER&Z<L4?AR*E&36XH4I`P>-\ZK*)>'=!9E2[BO0U9>]"O6P!E.]UAU[;
+MGN(0\H/(\AN:5G'FW4)D3^E<:NP&8905'/.8B5&`T!S9GDCAT?&$=,29LSPL
+MA\HQ_21$PP#O=9%9_^4I8`,:?Q7_X??_0EBQ_]U'G?PTUI[_36MY_]<T#??_
+M(GCL_)],`W'>QRWAOR;C_^G7WYPUUOF_;C16SG\&QO^%$(<BA.=.[%$1.DOS
+MMX`0_Q9VAC(CHD=&;[(-1'BY*T-,?UXX8+9/8?\`E#3Z5M[(VG`4N+-H(.\3
+M0UE]`4IF=U'@4K;;:Y%$J7(P"J@=T<$\-T@>QY5GXV3\/[D$REUC[?ZO&\OQ
+MGZFIZ/]%L+C_3RR/U_\(@B`(@B`(@B`(@B`(@B`(@B`(@B`(@B`(@B`(@B`(
+,\D_S"QCQWFL`4```
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/CHECKSUMS
new file mode 100644
index 0000000000..5d2a6d6ee3
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/CHECKSUMS
@@ -0,0 +1,20 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '986e4316ac095d8a4d47d0d0dd2c408a',
+ 'size' => 1541
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.readme b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.readme
new file mode 100644
index 0000000000..ba8894c152
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.readme
@@ -0,0 +1,2 @@
+README
+
diff --git a/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..148930eb47
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,50 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-CPAN/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("-\X34(``T9O;RU"87(M,"XP,2YT87(`[5K_3QI)%/=7YZ]XU39H(BN[
+M@%PP-*)N+:D"$6QL[AHRP``;EQVZ.RN2IO][WV,79+4]KG>(UW8^B<+NO)GW
+MYGV;F3>\D3)]S/UTQLB8^QM/@PRBD,_3IUG(FXN?,VR8&<O*6ME<-EO8R)A6
+M/GNP`?DGDB>!,%#<!]BXX9[X.[IE[;.)S#Y_$KQ9M/_)VW+US&ZLF@?IXR"7
+M^[[]K=P#^V?S5GX#UJ+$W]S^%>!#X!!;'GJ.*]ASRZ2Q/B3B_Z)<K;RQ&\T5
+M\U@6_R8F^P?Q?W!0T/&_#K0=;[\G99O[QLAEQZ'C=HWZ.8OS`7.=]CZZR/XQ
+MM0\3CW<!F_D+N[3+IQ<V4R)0-,R%W2P;DZ&K,\G_'HGX)_,^`8\?W_]ELYF<
+MWO^M`X_L3^&]8AX_;O]<QBIH^Z\#W[1_E-Y7QF/9^F\5'J[_>=/,Z?5_'=AV
+MO(X;=@5LV==-^[)J#+;8_;N1\-WDF^O&U3&]81>UTZMS&TJ`#E,LHL=L;M;+
+M)^_*9\EWC#F>8D[0$K?"VW&\4:AVV2:^`\+TF=&WD]JI762;EW;S??D<1XA(
+MX1584"I!9O=P2E6[:M:OFG,ZO;_XS_AN_(^&*^.Q)/XM*U=X&/\93`DZ_M>`
+M$>_<\+Z81^PA"P,!@?*=CCIDS!>?0L<78-^-I*\$-L_>G$X\?BYY5\1=;KD?
+MP*?Q#AQ5&F5X95_7:Y?-5K-\UH"C^*'V;O857KZW+QN56A4PL-FT1XDZS]@L
+MC$X$VU!18AB`DB"F%)@X\'N'NZY`KAX?B@`G(J`]@:[H\=!5!E2E$D7H2O"D
+MBKOA0%/:!3H8.VH@,=-PN!7^!/I2=L$7/)">`5<XKWO9'2]0@G<-'.4T&C5P
+MAB-W,I,)I8&)#'T8A6W7Z4`O]#K*D5ZP/Q3(HQOL=_!!<4\%!LVI.7`"ZB3'
+M`4K3<;G/B7R3M'G=:.)9"E)%;$\=D@)Z-/9L.IX075#8?P^&\M;Q^O3@]7$<
+M-$U'H4Q3!<VT+?T%&^!@8P=%#?BM@*$82G]BL(2],/M"BAA#Z37\279AFVP7
+M/D;&NE<(T1U]AI<+?3]/^WV!+XNTD6UIC$,V-WP)4I1S<'*L+:5"C^.CN1?.
+M_6-J_+HO7/*%+L2*1"O!0/@"U6@>LE;+KIZV6DAXC'1C0*T&*FRCKCKA4'AJ
+MJE7HH1*FUAG*;N@*`SZ@.MM"D;>)KH,NI5XP5AJ@A4VHEO$TRV(CI*&.RR!:
+M60DOF`W5=OG@_M^\8^-#M59O5!H,EZM[.]+B]9T.IW;CY+)2;^)<&6M\6^QH
+MD#WHH%\JU`)Z[\"Z"PR,"G"EO`G`=6X$NH!@/$0%^2![]+0@\IACF(B^Z_1Q
+M9!">#/L#"B=7D!<0+:F,A1ZI0J"/L^.$O$8LL!7'`V-5Z27BC<VG5+YJOJU=
+M,E;&`#+0RZ6_!]P(#9+LB!M][O*[B='#-8;^^)A/[M5GVU`^;]08HYW'CKE+
+M?#NX17CN-/G+(EG_B^LV*^:Q;/^?S68?G?\R>OU?"]+I-&Q_*%^<%TTCPVAU
+M+$+L$PP71$H>12#GB%-+$5-9&GX@M'F;4GM'%?]!%L55$QM1@M"[\>388YBM
+MA$\YK]6>%.%BFKEQ@:`J)<32H7#60>8/<V^^DM-LJ%CYW*K]*9"(_[A^NVH>
+MR^(_4WA<_\GI^O]:<&R?5:KP&48^G<FW3,,P__*VX$NTJ;\_%<3M\@:H_5`'
+MUR^"1/S39=`3\/A7]7_]^X^UX)']YY>!J^.Q-/_G\H_KOY;._^O`]HO],/"G
+MEJ<C%YLE^L@-,-'3"?NYA=1X,B3B/[K&7SF/I?%O/3S_97.FJ>-_'8A__Q59
+M7O_\Z[=#(OYG/_]9,8]E\6_EOG'_K^L_:P&=\A)U%5SQAQ-XV9[66$K@B?&#
+MN@L#V*'+V*B.WJ*2$=T3I&9GQ=0>M08=WQFI%B64(+I&2"5VERGX2'1T2Q#Q
+M2K^.RMNMZ5,KZJ^W'AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:
++R_`55?+KB0!0````
diff --git a/lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz.packed
new file mode 100644
index 0000000000..696ae1572b
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz.packed
@@ -0,0 +1,25 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz.packed lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz lib/CPANPLUS/t/dummy-CPAN/modules/02packages.details.txt.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("-%#OT4``S`R<&%C:V%G97,N9&5T86EL<RYT>'0`E=-1:]LP$`#@=_^*
+M>]C#!K&D.IB"GAJGR=B6=&5M:-^&:ET3,5LRTKE>]NLG-2TM(6FWX\!@WWVZ
+M`WEN&I3P'*+H5/U+K3$PC:1,$QC]IFSU8_%2`QNB3G(^#`/KT#>L=BV?7DXN
+M>.MTWV#@1Y1S#+4W'1EGHW:Y*P&K6@QP[WJKP5C0QF--SF_APZ.I>MHX'[C1
+M/)NZIF]M>!JE>P6,X`%]B/`HOJ9-]L426HTZGSLO8=*3:Q6AAGND>@/>]60L
+MAM'N\`@A:%?W+5I2:3R6W7A#D<BK;3JM;;0)-*C4_/$!"C86Q2>XV\+$:H\J
+ML&\.K5F?*6M:%7?.%I'/IW$G2NTGXK0LLX4*E*\ZG0:1<(-Z!&,!7Y6%0H@"
+M1"F+F*?P>7F=97/GI*R4EW*VDO+V"O9",'&2GC,^6\6\O>*Q(X\->?K"2'FV
+M_O-*659O*4N^K&+^DW+Q?=_94U+!.T[:Z;CSM-.[SOXV_^M4\<K%RR^/>8><
+B74]^B)L.^MA$CU$<X-+_4S(ARI]B_.QD?P$Z!("8DP,`````
diff --git a/lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz.packed b/lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz.packed
new file mode 100644
index 0000000000..e4fb69c0a1
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz.packed
@@ -0,0 +1,28 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz.packed lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz lib/CPANPLUS/t/dummy-CPAN/modules/03modlist.data.gz.packed
+
+Created at Sat Apr 7 13:06:48 2007
+#########################################################################
+__UU__
+M'XL("#'FO$4``S`S;6]D;&ES="YD871A`%U3_6O;,!#].?HKCBXC"20A=<@&
+M"@OIYV!;/Z`-&V1E*/:U%I4M(\D-)N1_WTFVFVS&H.=W>N^>SO:U5,BAN2;3
+M3"=*6C=.A!/L$FUL9.&DSCD\IF@1A$%P*8*O$Q`N,$6Y(56*"<@\E,FF5,@Z
+MWFL(F])YMH),5+#Q5=(8C#'WO*@E2CBT#@IM'2:L4SN$+'#WA@:<S!"VV%.*
+M;%X1;!FBB&!MZT2L$Y,;=2@M97$:"B-##U*F6E%08>AQJT$_>_;?-A>E,11)
+M5522%NC.=2V.A<4QN]%)K,O<A7%%T]F,_332.<Q'YQ5QF4K(9BM<G$+_#:+Q
+M=!(-8%/!69X8%';\76,N7Y8BEYD8)\@NZ<3OH[\V<@C1#+[1`:+))(+))QY]
+MYI,I?+UY9,QO*43\*EX0+N[/;CF_>4\^#]4/L+)4Y<V9R5QP?EEF!9K1(L=M
+M?_V_;K3P,WL:C!9^&V@#5F92"1/\;+FIW_*.U0FS"KH&;:D<?('=?GY$4\M,
+MF(KX$QJH3$[:XC.9]I==[S/8T;0K^)@*F\Y99^G7W;(;:V7W)%QV_Q#;-!@M
+M=MU0;YWW?LOO1EM;[YNUD<SK&>T9"Y:T?4U$+\3I#3VT3K@C:`]0':`\P**&
+MR>$GJ`GZMDSK&:>B<.&1/5&"<-"F]3KDZ5UKS?FY,)Q?K3B_O?OU$)1>VX*D
+K!44+[EK0KO[5W?]8/<"C_T?NZP^A+5ZMCFU/3WL$GH8^T%]3O>X%W0,`````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz.packed
new file mode 100644
index 0000000000..34ac29d6ee
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz.packed
@@ -0,0 +1,19 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz.packed lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz lib/CPANPLUS/t/dummy-localmirror/01mailrc.txt.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("%_EO$4``S`Q;6%I;')C+G1X=`!+S,E,+%9P#8T(5@`#)=>*DM"2S)QB
+M*RO?Q.Q4$"Y2`$HZYZ>D*M@D%R3F%>24%CND5B3F%N2DZB7GY]HI<25"#?'S
+MAQB#U1"_?"+-\76".\8W/Z4T)]7*RJDT,R>%:.UP9Z!J]_,G:`(`W)]=R`X!
+"````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz.packed
new file mode 100644
index 0000000000..472cbde8b8
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz.packed
@@ -0,0 +1,25 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz.packed lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz lib/CPANPLUS/t/dummy-localmirror/02packages.details.txt.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("-%#OT4``S`R<&%C:V%G97,N9&5T86EL<RYT>'0`E=-1:]LP$`#@=_^*
+M>]C#!K&D.IB"GAJGR=B6=&5M:-^&:ET3,5LRTKE>]NLG-2TM(6FWX\!@WWVZ
+M`WEN&I3P'*+H5/U+K3$PC:1,$QC]IFSU8_%2`QNB3G(^#`/KT#>L=BV?7DXN
+M>.MTWV#@1Y1S#+4W'1EGHW:Y*P&K6@QP[WJKP5C0QF--SF_APZ.I>MHX'[C1
+M/)NZIF]M>!JE>P6,X`%]B/`HOJ9-]L426HTZGSLO8=*3:Q6AAGND>@/>]60L
+MAM'N\`@A:%?W+5I2:3R6W7A#D<BK;3JM;;0)-*C4_/$!"C86Q2>XV\+$:H\J
+ML&\.K5F?*6M:%7?.%I'/IW$G2NTGXK0LLX4*E*\ZG0:1<(-Z!&,!7Y6%0H@"
+M1"F+F*?P>7F=97/GI*R4EW*VDO+V"O9",'&2GC,^6\6\O>*Q(X\->?K"2'FV
+M_O-*659O*4N^K&+^DW+Q?=_94U+!.T[:Z;CSM-.[SOXV_^M4\<K%RR^/>8><
+B74]^B)L.^MA$CU$<X-+_4S(ARI]B_.QD?P$Z!("8DP,`````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz.packed
new file mode 100644
index 0000000000..d4f9e78237
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz.packed
@@ -0,0 +1,28 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz.packed lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz lib/CPANPLUS/t/dummy-localmirror/03modlist.data.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("#'FO$4``S`S;6]D;&ES="YD871A`%U3_6O;,!#].?HKCBXC"20A=<@&
+M"@OIYV!;/Z`-&V1E*/:U%I4M(\D-)N1_WTFVFVS&H.=W>N^>SO:U5,BAN2;3
+M3"=*6C=.A!/L$FUL9.&DSCD\IF@1A$%P*8*O$Q`N,$6Y(56*"<@\E,FF5,@Z
+MWFL(F])YMH),5+#Q5=(8C#'WO*@E2CBT#@IM'2:L4SN$+'#WA@:<S!"VV%.*
+M;%X1;!FBB&!MZT2L$Y,;=2@M97$:"B-##U*F6E%08>AQJT$_>_;?-A>E,11)
+M5522%NC.=2V.A<4QN]%)K,O<A7%%T]F,_332.<Q'YQ5QF4K(9BM<G$+_#:+Q
+M=!(-8%/!69X8%';\76,N7Y8BEYD8)\@NZ<3OH[\V<@C1#+[1`:+))(+))QY]
+MYI,I?+UY9,QO*43\*EX0+N[/;CF_>4\^#]4/L+)4Y<V9R5QP?EEF!9K1(L=M
+M?_V_;K3P,WL:C!9^&V@#5F92"1/\;+FIW_*.U0FS"KH&;:D<?('=?GY$4\M,
+MF(KX$QJH3$[:XC.9]I==[S/8T;0K^)@*F\Y99^G7W;(;:V7W)%QV_Q#;-!@M
+M=MU0;YWW?LOO1EM;[YNUD<SK&>T9"Y:T?4U$+\3I#3VT3K@C:`]0':`\P**&
+MR>$GJ`GZMDSK&:>B<.&1/5&"<-"F]3KDZ5UKS?FY,)Q?K3B_O?OU$)1>VX*D
+K!44+[EK0KO[5W?]8/<"C_T?NZP^A+5ZMCFU/3WL$GH8^T%]3O>X%W0,`````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..b76bf9d75b
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,34 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Bundle-Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("!1%OT4"`T)U;F1L92U&;V\M0F%R+3`N,#$N=&%R`.V7:V_:,!2&^8I_
+MQ6F91"L52"`)6KI6`Q8TI`$5EZW2-"%##%C-K8FSEDW[[W,2:&E$6VGKH$Q^
+M(`KR<4YB7K_'3CUT3(L4FJY;J&._(!4EN91Y821.5563LY:<.:MS_%N65+6J
+M*67^R4BR(E<J&5`S6R`,&/8!,E?8(4_UNYD38CTSR+5!9?:$^@;]+3HN[5C_
+MJB0+_7>I?])>VI'^FBR7A?ZO0'_>7MJ%_EI%%?J_$OUY>]&S_U)_35$>U5^5
+MM'O]I2K7OQK7?TGH_\_Q\.0*SP@D>NLZ%US7N>*G"+WY;/3ZK6X'SN`PFA>'
+MO$WFQVAD=#Z,1@B=S0DV9>C4V@9"Z01P%V]T.P.C,^@CM(KINC'4]<L^<*+,
+MZ4BG&\72D7;]L6NBR/HUC1L3TB21Y2/5AH./W1Y"QBT;,FH%/`6^(M'A0\<%
+MGJGAF@3>33SL>%88O">WV/8L4IRX]CG/,@D9']_]/Y'96S;Y/_H?II2/]N+3
+MBZW_3_I?45?^E\OE<KS^2XKP_W:&3V"#"TY1%`B83R>,6_Z+3QE930LX0I&A
+M(MNO&^SL'/+I(I`_B;LN"\FHV>NV5UTW+C+Y$\C!E#IF`*OJ$R<P:<!2]_K)
+MZTK[HF?T^W&ZV0_J0>'ME&?H#YO-UN6R-0^_DF=(+/_P<?_(_GQ,V5J]/^C5
+M&H.U9-=?TX/_=H*.3Z,HVC?_USJMIM$?O.3[WU/^+\O5U/Y/+6O"_UMAHPW1
+MV@J`5K,AF\T.YC0`*_*B.X4H'J"V,:@5%[8%S]%VS9#7#ILP7#`QPW"$39.8
+M,%[`G?F.]W@A_9_6_Z6FV_*_4DV__ZF*I`K_;X,<S!GS]%+)COU9&(?4,HN!
+M&_H3,G7]&2DZA,4SHA!X9%*<,]M"N<L$B`L"_V+P?)>Y;.&1@X,#@!:#&VI9
+M,)ECA[]<4`?8G,`T9*$?=XBOSB$'VT2_JQ`/IR+Z3OR`NHZ^MGU?-HVFOFOK
+ML+ET48<+:EDF]0-^:<#W+<@GUR'U2:`C%&TD?#H.690F>EP=DH&C&7&(CQDQ
+H1^.%OFE+!,N[@U:L2*)0"00"@4`@$`@$`H%`(-@;?@-?EF['`"@`````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/CHECKSUMS
new file mode 100644
index 0000000000..e716d3635c
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/CHECKSUMS
@@ -0,0 +1,30 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '2917421f5a41419f7bb2d2cf87f04b8d',
+ 'size' => 1066
+ },
+ 'perl5.005_03.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '2b70961796a2ed7ca21fbf7e0c615643',
+ 'size' => 119
+ },
+ 'Bundle-Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '76f9c0eed0de9f533ed4d3922bac2f11',
+ 'size' => 850
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..52c855198b
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,39 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("/8X34("`T9O;RU"87(M,"XP,2YT87(`[9KQ;]I&%,?Y^?Z*1YE$(A5C
+M8YM(SE*-K69!*LD4Z%:IJZ(#'V!A^[SSN2F:]K_O#ML,6)ONAP+M\CY*XMCW
+MS/EX]WWWGNT^YZT?J6B9AFFU:X?!5%RXKMY:%ZZUO:VH66:G8W=LQ^YV:Z;5
+M<1VS!F[M".29I`*@MJ0)>\SN<^W50*KM-T)_V_\_7?=N?O9'A_"_XSB?]G_'
+M*?VO)L"%]K_M=MP:F.C_@S,`&@.%TO,P"R-&:LB384?_P][-H.^/Q@?0?_<1
+M_7?L_?BO_CJH_V-0"I_<^;V70Y\,Z9+I&&#\\HI4LX%(EDDCC4@43MIJOK35
+M?#'2F$S"I#WC?*+W(C+TQSUC%4?P.88\R",&,9.T%5!)X8P&`0M@L@+=N_X5
+MYQB#3J+_TH='UK_C=O;T[Z@4$/5_#!JPD#+UVNUX+<O6)`^CP,AX+J9LQL6<
+M&0F3ZXG1RE(V-18RCDCC30&,%V$&ZH="*KCD<I6R>KT.,)#P$$813!<TF3,(
+M$Y`+!K-<YF)ML#Z[01(:,V\3&,JI2-XSD84\*1OTQ*P.W<\$CSW8BT-AHEP8
+M14$H,G5.%DI&!/LC#P7+/$*",),BG.12GZ\OT(-BJ&3.$B:H9,']9.6!_T&^
+MEF&4>=XF"D'9+72-CDN>@/[+0'_T_+]K[>7_CNE<H/Z/HO]Z.\]$6Z_E*1,1
+M(:D($PG/+,.P?D^>75;[?`G%/BZ9_U_]Z\#Z5=S_L6VSB_=_3N)_O;">W/^.
+M:5OH_Y/YOTBLCK7^:]?OK?^NB?7_<2!7"T8#"VYZJOHG(QJGJC9OZ5P<5&T/
+M67%`_4LJR][K\?7MW<9V2!/XOC#[@7U8;XTICU]L[&_'U[XRKW+I*$Q8IBN"
+ME`>0+7@>J=)?E0CSA`L6>.2[7_V[T>#V!JZ@F7`)?.G!7&VK\W4)H,^M-R]5
+M%]-<JI2%3I=4E1GJHCU/S5W5T(!_]:<N*F:)S#[6:0/^4[?E1ZR[;JB2IBA^
+M=&FSW1L\L*9@$'&^#).Y^N[$SJC.FEIKS?.WYKM+@,;6Y;#W-&H&A/Q)]/$'
+M5C7Q)%J!5!5)446I.D?N=CCC>:)&`:K/*8V(+IL^-9Z,37D2[`\K8'I,?Q%B
+M88+WA/,_705\)?F?A<__3N/_S1W=+^M_]S'_.^[^^F]9^/SOI/5_,0U4O8]+
+MPM.)_UM/?[YT_'],_QW7WK__YUHVZO\XPV<?N_6M=/^;""6K9L39.JG41<+V
+MH[RK%]"LDN[F\[5)F7;>]^]NAY7);EE9&OIO_/O^X)4_^N>SWC9W%J#FN^>Z
+M@9Q?K@TP"AU<_\5#X(/D?X^__[.O?]NQ+-3_,2C?_RD\CZ__(`B"(`B"(`B"
+?(`B"(`B"(`B"(`B"(`B"(`B"(,@WP=^)5B"Y`%``````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed
new file mode 100644
index 0000000000..34c30ca383
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed
@@ -0,0 +1,18 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUNOXS/perl5.005_03.tar.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL(`'3DO44``^W/,0J`,`Q`T1RE)Y"T-O4XXN"DB%2]OR(*NNC4[;_E#\F0
+MS'T>K5*U5FLI1`\IQK--LE<OXM6LJ5/P,8CZ8$'%::F#GK9E[;)S,G13_[7W
+=-[\?N0L`````````````````0$$[-9`]0P`H````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/CHECKSUMS
new file mode 100644
index 0000000000..f124759db0
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/CHECKSUMS
@@ -0,0 +1,20 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => 'c7691a12e5faa70b3a0e83402d279bd6',
+ 'size' => 1589
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..9516f202e1
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,51 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-localmirror/authors/id/E/EU/EUXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("`DY34("`T9O;RU"87(M,"XP,2YT87(`[5IK3]M(%,U7YE=<H%5``A.;
+M/*0@5@0P-"IY*`E=JFX53>))8F%[4L\8B%#_^][Q(P^VI5\VZ78[1P0G]IWG
+MO>?,^-I7G!^>T_"P8!3,H]QZ4$!42B5U-"LE<_F8(6<6+.O8.BX633QO6B6K
+MDH-2;@.(A*0A0.Z>!NPUNQ]=SP:2'7\17"W[_^)=K7EM=]?A_V*Q^'W_6\74
+M_Q@`E3+Z_[ADE7)0T/Y?.^I`?:"0>AY&KL=(3N.WP0K_&[5F_<KN]C:L_Q:*
+M_:K^'Y?+1<W_32`E/NG8M<N&3;(((`UZSY08&.T;@O%A3/WX\"2(9$(:4X\,
+MW.!HQ/E`7?1(P^[5C)GOP8_0X$[D,?"9I(<.E13VJ.,P!P8S4&VJ3[BO)>CG
+M\#]UXAKX7WYE_2\6K27^%Q3_*Y6*YO\FL`L3*:?5HR,_YN7A('(]QQ`\"H=L
+MQ,,Q,P(FX\`X%%,V-";2]\CN70+H35P!^$=A&G+)Y6S*MK>W`>H2'EW/@^&$
+M!F,&;@!RPF`4R2B,#>+2NR2@/JO.E2$-1?+`0N'R(+V@`C,[U1^%W*]"JD=N
+M@*[S/,<-!=H*5S(2LB^1&S)1)<1QA0S=0215.=6Q*B1#)&,6L)!*YO0'LRK8
+M3_)6NIZH5N?R`VES4#:L$OF-^)]*^SKV_Z77[O\JA1?[_V*A9&G^;P+G]G6]
+M"<_(7S>0L&,:AOE7L`-?3T@DF*)DM8KA<4+2Z_P>U/43O4#_'_FOMG3_B?S/
+ML571^9^?X__YEGZ#^E\LO=#_DFGJ_,]F]G_;1Y$(8\]/6>B13.B3,$"A)Z86
+M^]]%_Y,[_+7H_VO\MY#S+_*_9:NL^;\1_KO!T(L<!COV7<_N-(W)#EF<4Y*P
+M>N:N>WNNSI!&Z_+VQH;3^1YQ:ZM=NWA?NUX]1_`V31)7]-D#"_;<8!K)?;*E
+M)$8A_DW4MXO6I5TE6QV[]Z%V@S4DIO`6+#@]A<+^26S5NNVU;WMS.RU,_S[_
+MI_ZF^6]9Q<H_^&\6-/\W@2D=WM/Q\IV>NN]3J9.AQ,4_3:B`_33EH61X.3MS
+M.0OH#:<.2XL\T%#`E\<].*MW:_#6OFNW.KU^KW;=A;/T1^M]]A7>?+`[W7JK
+M"4AL$I<X586S9I9J5P:[4)?,%R`YL-@"A0._#ZGG,6Q5I9$$#H2I)++#1C3R
+MI`%-+ED5'`X!EVDQK"BV7;*#1U=.."H-55F?&8PY=R!D5/#`@%L<UZ+O*M_$
+MJ&-@+9=)K<+UI]XLZQ/V!F8\"F$:#3QW"*,H&*KLDSCR&;;AB*,A5RFK0`I#
+MC2E.GF$A_BBP-T./AE29;ZG9O.OVF)"0K^+U_(F:@)&J.QM.P)@#$LL?@,\?
+MW&"L?@1CK`==,Y38IWB"LMGFX9(/L+(X-R?H@TK#^SR<&63%7ZB^D%<-P^D?
+M\$GYA6R1??B<.&LQ(<KN[!G>+)5]CLM]A:_+MHEO51TG9.[X4\@KS<'!D0'G
+M$B..3N=1.(^/V/GMD'DJ%AQ()Q*]!!,6,B/>G?;[=O.RWT?#<[1[5/E((:,!
+MSM4P\ED@XUF%$4Y"[)TD"VC`1YS.`9,JVICC8DC);4).)^AA$YJUADU(ZH1#
+M:.,RB%Z6+!!950./3A;_Y@6['YNM=K?>);A<+?RH%J_O%+BTNQ>=>KN'8R6D
+M^^UN)Y4<P!#C4B:/2B;6DS!4FM7C_%Z`Y]XSE6(E-,()"H&/XH3KHLN/%&G"
+MQIX[QIJ!!3P:3Q2=/*:B0-FJ*2-1H*:"88R3\Y7^&FF'K90/A#1YL,(W,A]2
+M[;;WKM4AI(8$,C#*>7@`U(@,U;,S:HRI1Y]FQ@C7&O6ACW2VF#[;AMI-MX6W
+M(3CE>^:^:G>(6P2]4&]B_5]ZZ+?1_7_Y^&7^MV3JY[^YS0R??>L1".KNGR$J
+M0181>_'V.Z]T,;]XEHO+0SZ3[/Q!8I(*=_^JTVKD$Y-D6YD_@%T8N0'*=Z;N
+M<0G[SNY?U6_L[J+23_F53%3^\T'6HEI"-&G7Q?_D)8"U[/]??__K)?^/51I8
+M\W\#2-__2CRO7__2T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0T-#0
+.T-#X9?`W%LHWQP!0````
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/CHECKSUMS
new file mode 100644
index 0000000000..042008cc56
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/CHECKSUMS
@@ -0,0 +1,20 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '1f52c2e83140814f734c8674e8fae53f',
+ 'size' => 867
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..fe43f9239b
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,35 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBNOXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("-<X34(``T9O;RU"87(M,"XP,2YT87(`[9E;;]HP%,=Y]J<X+9722@-R
+M!0G6:G2#M=)*I]+MI:M0((9&)#;*I5TU[;O/)J1JH!WK%-)U/;\7XTOXVSD^
+M]G'<Y;QR:`<5M:IJM=)F4`4-RY*IUK"T^VE*25-UW=`-T]`;)573+<,J@;6A
+M_F2(P\@.`$I3F]'?M5M7GPXD35\(W?OV?W_4[GWL]//6D.^C;IJ/VU\WE^QO
+M6*(("GF)K]S^QV#[8,/"\C!V/4J>NT](<63\_Z3=.^YV^N<Y:ZSS?TVM+_M_
+MO6Z@_Q?!PO')6:?]X:1##F/7<ZJ?/Y%T*I"(AE%UYA'/'=;$9*F)R5*=^63H
+MLMJ8\Z',>;ABO%@R_K^P==X::_?_NK;D_Z9JUM'_BZ"\58O#H";=>48#CY!9
+MX+((MK5J5?O&MEMIGD\AR3]WAY%<R?B_7.,WH/'T\Y]AJ`:>_XI@Q?YRC\]9
+MX^GV-U6]CO8O@@?MG\1XN6FLV?^EZ9?L;ZD8_Q<#V;^BMJ-!KRVB?]*W_9E'
+MH0)B%H`([R%,"L1/DK9L?SD_.CV[:WMB,WB;-'M'O\_3ZHC[!W?M3\^/.J+Y
+M-0U"ES/P7$9#<!G,N`/A%8\]!X84W`GC`76:9.=KYZQ_?-J#?5`8CX!/FS`1
+M:?K\.."^?'9+:0F)41R)D,4>3>T)E9UN-L7<%15E6-$3G?(IB\*'1,OP1[*+
+MOYA+ER&Z<L4?AR*E&36XH4I`P>-\ZK*)>'=!9E2[BO0U9>]"O6P!E.]UAU[;
+MGN(0\H/(\AN:5G'FW4)D3^E<:NP&8905'/.8B5&`T!S9GDCAT?&$=,29LSPL
+MA\HQ_21$PP#O=9%9_^4I8`,:?Q7_X??_0EBQ_]U'G?PTUI[_36MY_]<T#??_
+M(GCL_)],`W'>QRWAOR;C_^G7WYPUUOF_;C16SG\&QO^%$(<BA.=.[%$1.DOS
+MMX`0_Q9VAC(CHD=&;[(-1'BY*T-,?UXX8+9/8?\`E#3Z5M[(VG`4N+-H(.\3
+M0UE]`4IF=U'@4K;;:Y%$J7(P"J@=T<$\-T@>QY5GXV3\/[D$REUC[?ZO&\OQ
+MGZFIZ/]%L+C_3RR/U_\(@B`(@B`(@B`(@B`(@B`(@B`(@B`(@B`(@B`(@B`(
+,\D_S"QCQWFL`4```
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/CHECKSUMS b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/CHECKSUMS
new file mode 100644
index 0000000000..5d2a6d6ee3
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/CHECKSUMS
@@ -0,0 +1,20 @@
+0&&<<''; # this PGP-signed message is also valid perl
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+# CHECKSUMS file written on Tue Jun 15 02:56:25 2004 by CPAN::Checksums (v1.016)
+$cksum = {
+ 'Foo-Bar-0.01.tar.gz' => {
+ 'mtime' => '1999-05-13',
+ 'md5' => '986e4316ac095d8a4d47d0d0dd2c408a',
+ 'size' => 1541
+ },
+};
+__END__
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.2.3 (GNU/Linux)
+
+iD8DBQFAzmVZMo2oZ0UPiewRAv5MAJ9tajbtrg14echRtn4t940v7FpMfgCffvU1
+mAcaUP8yzmIvbpdn1cGUgpw=
+=rrmL
+-----END PGP SIGNATURE-----
diff --git a/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed
new file mode 100644
index 0000000000..ed67a73a41
--- /dev/null
+++ b/lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed
@@ -0,0 +1,50 @@
+#########################################################################
+This is a binary file that was packed with the 'uupacktool.pl' which
+is included in the Perl distribution.
+
+To unpack this file use the following command:
+
+ uupacktool.pl -u lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz
+
+To recreate it use the following command:
+
+ uupacktool.pl -p lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz lib/CPANPLUS/t/dummy-localmirror/authors/id/M/MB/MBXS/Foo-Bar-0.01.tar.gz.packed
+
+Created at Sat Apr 7 13:06:49 2007
+#########################################################################
+__UU__
+M'XL("-\X34(``T9O;RU"87(M,"XP,2YT87(`[5K_3QI)%/=7YZ]XU39H(BN[
+M@%PP-*)N+:D"$6QL[AHRP``;EQVZ.RN2IO][WV,79+4]KG>(UW8^B<+NO)GW
+MYGV;F3>\D3)]S/UTQLB8^QM/@PRBD,_3IUG(FXN?,VR8&<O*6ME<-EO8R)A6
+M/GNP`?DGDB>!,%#<!]BXX9[X.[IE[;.)S#Y_$KQ9M/_)VW+US&ZLF@?IXR"7
+M^[[]K=P#^V?S5GX#UJ+$W]S^%>!#X!!;'GJ.*]ASRZ2Q/B3B_Z)<K;RQ&\T5
+M\U@6_R8F^P?Q?W!0T/&_#K0=;[\G99O[QLAEQZ'C=HWZ.8OS`7.=]CZZR/XQ
+MM0\3CW<!F_D+N[3+IQ<V4R)0-,R%W2P;DZ&K,\G_'HGX)_,^`8\?W_]ELYF<
+MWO^M`X_L3^&]8AX_;O]<QBIH^Z\#W[1_E-Y7QF/9^F\5'J[_>=/,Z?5_'=AV
+MO(X;=@5LV==-^[)J#+;8_;N1\-WDF^O&U3&]81>UTZMS&TJ`#E,LHL=L;M;+
+M)^_*9\EWC#F>8D[0$K?"VW&\4:AVV2:^`\+TF=&WD]JI762;EW;S??D<1XA(
+MX1584"I!9O=P2E6[:M:OFG,ZO;_XS_AN_(^&*^.Q)/XM*U=X&/\93`DZ_M>`
+M$>_<\+Z81^PA"P,!@?*=CCIDS!>?0L<78-^-I*\$-L_>G$X\?BYY5\1=;KD?
+MP*?Q#AQ5&F5X95_7:Y?-5K-\UH"C^*'V;O857KZW+QN56A4PL-FT1XDZS]@L
+MC$X$VU!18AB`DB"F%)@X\'N'NZY`KAX?B@`G(J`]@:[H\=!5!E2E$D7H2O"D
+MBKOA0%/:!3H8.VH@,=-PN!7^!/I2=L$7/)">`5<XKWO9'2]0@G<-'.4T&C5P
+MAB-W,I,)I8&)#'T8A6W7Z4`O]#K*D5ZP/Q3(HQOL=_!!<4\%!LVI.7`"ZB3'
+M`4K3<;G/B7R3M'G=:.)9"E)%;$\=D@)Z-/9L.IX075#8?P^&\M;Q^O3@]7$<
+M-$U'H4Q3!<VT+?T%&^!@8P=%#?BM@*$82G]BL(2],/M"BAA#Z37\279AFVP7
+M/D;&NE<(T1U]AI<+?3]/^WV!+XNTD6UIC$,V-WP)4I1S<'*L+:5"C^.CN1?.
+M_6-J_+HO7/*%+L2*1"O!0/@"U6@>LE;+KIZV6DAXC'1C0*T&*FRCKCKA4'AJ
+MJE7HH1*FUAG*;N@*`SZ@.MM"D;>)KH,NI5XP5AJ@A4VHEO$TRV(CI*&.RR!:
+M60DOF`W5=OG@_M^\8^-#M59O5!H,EZM[.]+B]9T.IW;CY+)2;^)<&6M\6^QH
+MD#WHH%\JU`)Z[\"Z"PR,"G"EO`G`=6X$NH!@/$0%^2![]+0@\IACF(B^Z_1Q
+M9!">#/L#"B=7D!<0+:F,A1ZI0J"/L^.$O$8LL!7'`V-5Z27BC<VG5+YJOJU=
+M,E;&`#+0RZ6_!]P(#9+LB!M][O*[B='#-8;^^)A/[M5GVU`^;]08HYW'CKE+
+M?#NX17CN-/G+(EG_B^LV*^:Q;/^?S68?G?\R>OU?"]+I-&Q_*%^<%TTCPVAU
+M+$+L$PP71$H>12#GB%-+$5-9&GX@M'F;4GM'%?]!%L55$QM1@M"[\>388YBM
+MA$\YK]6>%.%BFKEQ@:`J)<32H7#60>8/<V^^DM-LJ%CYW*K]*9"(_[A^NVH>
+MR^(_4WA<_\GI^O]:<&R?5:KP&48^G<FW3,,P__*VX$NTJ;\_%<3M\@:H_5`'
+MUR^"1/S39=`3\/A7]7_]^X^UX)']YY>!J^.Q-/_G\H_KOY;._^O`]HO],/"G
+MEJ<C%YLE^L@-,-'3"?NYA=1X,B3B/[K&7SF/I?%O/3S_97.FJ>-_'8A__Q59
+M7O_\Z[=#(OYG/_]9,8]E\6_EOG'_K^L_:P&=\A)U%5SQAQ-XV9[66$K@B?&#
+MN@L#V*'+V*B.WJ*2$=T3I&9GQ=0>M08=WQFI%B64(+I&2"5VERGX2'1T2Q#Q
+M2K^.RMNMZ5,KZJ^W'AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:&AH:
++R_`55?+KB0!0````
diff --git a/lib/CPANPLUS/t/inc/conf.pl b/lib/CPANPLUS/t/inc/conf.pl
new file mode 100644
index 0000000000..7ca87471af
--- /dev/null
+++ b/lib/CPANPLUS/t/inc/conf.pl
@@ -0,0 +1,173 @@
+BEGIN {
+ use FindBin;
+ use File::Spec;
+
+ ### paths to our own 'lib' and 'inc' dirs
+ ### include them, relative from t/
+ my @paths = map { "$FindBin::Bin/$_" } qw[../lib inc];
+
+ ### absolute'ify the paths in @INC;
+ my @rel2abs = map { File::Spec->rel2abs( $_ ) }
+ grep { not File::Spec->file_name_is_absolute( $_ ) } @INC;
+
+ ### use require to make devel::cover happy
+ require lib;
+ for ( @paths, @rel2abs ) {
+ my $l = 'lib';
+ $l->import( $_ )
+ }
+
+ use Config;
+
+ ### and add them to the environment, so shellouts get them
+ $ENV{'PERL5LIB'} = join ':',
+ grep { defined } $ENV{'PERL5LIB'}, @paths, @rel2abs;
+
+ ### add our own path to the front of $ENV{PATH}, so that cpanp-run-perl
+ ### and friends get picked up
+ $ENV{'PATH'} = join $Config{'path_sep'},
+ grep { defined } "$FindBin::Bin/../bin", $ENV{'PATH'};
+
+ ### Fix up the path to perl, as we're about to chdir
+ ### but only under perlcore, or if the path contains delimiters,
+ ### meaning it's relative, but not looked up in your $PATH
+ $^X = File::Spec->rel2abs( $^X )
+ if $ENV{PERL_CORE} or ( $^X =~ m|[/\\]| );
+
+ ### chdir to our own test dir, so we know all files are relative
+ ### to this point, no matter whether run from perlcore tests or
+ ### regular CPAN installs
+ chdir "$FindBin::Bin" if -d "$FindBin::Bin"
+}
+
+BEGIN {
+ use IPC::Cmd;
+
+ ### Win32 has issues with redirecting FD's properly in IPC::Run:
+ ### Can't redirect fd #4 on Win32 at IPC/Run.pm line 2801
+ $IPC::Cmd::USE_IPC_RUN = 0 if $^O eq 'MSWin32';
+ $IPC::Cmd::USE_IPC_RUN = 0 if $^O eq 'MSWin32';
+}
+
+use strict;
+use CPANPLUS::Configure;
+
+use File::Path qw[rmtree];
+use FileHandle;
+use File::Basename qw[basename];
+
+{ ### Force the ignoring of .po files for L::M::S
+ $INC{'Locale::Maketext::Lexicon.pm'} = __FILE__;
+ $Locale::Maketext::Lexicon::VERSION = 0;
+}
+
+# prereq has to be in our package file && core!
+use constant TEST_CONF_PREREQ => 'Cwd';
+use constant TEST_CONF_MODULE => 'Foo::Bar::EU::NOXS';
+use constant TEST_CONF_INST_MODULE => 'Foo::Bar';
+use constant TEST_CONF_INVALID_MODULE => 'fnurk';
+
+### we might need this Some Day when we're installing into
+### our own sandbox. see t/20.t for details
+# use constant TEST_INSTALL_DIR => do {
+# my $dir = File::Spec->rel2abs( 'dummy-perl' );
+#
+# ### clean up paths if we are on win32
+# ### dirs with spaces will be.. bad :(
+# $^O eq 'MSWin32'
+# ? Win32::GetShortPathName( $dir )
+# : $dir;
+# };
+
+# use constant TEST_INSTALL_DIR_LIB
+# => File::Spec->catdir( TEST_INSTALL_DIR, 'lib' );
+# use constant TEST_INSTALL_DIR_BIN
+# => File::Spec->catdir( TEST_INSTALL_DIR, 'bin' );
+# use constant TEST_INSTALL_DIR_MAN1
+# => File::Spec->catdir( TEST_INSTALL_DIR, 'man', 'man1' );
+# use constant TEST_INSTALL_DIR_MAN3
+# => File::Spec->catdir( TEST_INSTALL_DIR, 'man', 'man3' );
+# use constant TEST_INSTALL_DIR_ARCH
+# => File::Spec->catdir( TEST_INSTALL_DIR, 'arch' );
+#
+# use constant TEST_INSTALL_EU_MM_FLAGS =>
+# ' INSTALLDIRS=site' .
+# ' INSTALLSITELIB=' . TEST_INSTALL_DIR_LIB .
+# ' INSTALLSITEARCH=' . TEST_INSTALL_DIR_ARCH . # .packlist
+# ' INSTALLARCHLIB=' . TEST_INSTALL_DIR_ARCH . # perllocal.pod
+# ' INSTALLSITEBIN=' . TEST_INSTALL_DIR_BIN .
+# ' INSTALLSCRIPT=' . TEST_INSTALL_DIR_BIN .
+# ' INSTALLSITEMAN1DIR=' . TEST_INSTALL_DIR_MAN1 .
+# ' INSTALLSITEMAN3DIR=' . TEST_INSTALL_DIR_MAN3;
+
+
+sub gimme_conf {
+ my $conf = CPANPLUS::Configure->new();
+ $conf->set_conf( hosts => [ {
+ path => 'dummy-CPAN',
+ scheme => 'file',
+ } ],
+ );
+ $conf->set_conf( base => 'dummy-cpanplus' );
+ $conf->set_conf( dist_type => '' );
+ $conf->set_conf( signature => 0 );
+
+ _clean_test_dir( [
+ $conf->get_conf('base'),
+# TEST_INSTALL_DIR_LIB,
+# TEST_INSTALL_DIR_BIN,
+# TEST_INSTALL_DIR_MAN1,
+# TEST_INSTALL_DIR_MAN3,
+ ], 1 );
+
+ return $conf;
+};
+
+my $fh;
+my $file = ".".basename($0).".output";
+sub output_handle {
+ return $fh if $fh;
+
+ $fh = FileHandle->new(">$file")
+ or warn "Could not open output file '$file': $!";
+
+ $fh->autoflush(1);
+ return $fh;
+}
+
+sub output_file { return $file }
+
+### whenever we start a new script, we want to clean out our
+### old files from the test '.cpanplus' dir..
+sub _clean_test_dir {
+ my $dirs = shift || [];
+ my $verbose = shift || 0;
+
+ for my $dir ( @$dirs ) {
+
+ my $dh;
+ opendir $dh, $dir or die "Could not open basedir '$dir': $!";
+ while( my $file = readdir $dh ) {
+ next if $file =~ /^\./; # skip dot files
+
+ my $path = File::Spec->catfile( $dir, $file );
+
+ ### directory, rmtree it
+ if( -d $path ) {
+ print "Deleting directory '$path'\n" if $verbose;
+ eval { rmtree( $path ) };
+ warn "Could not delete '$path' while cleaning up '$dir'" if $@;
+
+ ### regular file
+ } else {
+ print "Deleting file '$path'\n" if $verbose;
+ 1 while unlink $path;
+ }
+ }
+
+ close $dh;
+ }
+
+ return 1;
+}
+1;
diff --git a/utils.lst b/utils.lst
index 7de9fbbac2..4e88e43431 100644
--- a/utils.lst
+++ b/utils.lst
@@ -23,6 +23,9 @@ utils/pl2pm
utils/prove
utils/ptar
utils/ptardiff
+utils/cpanp-run-perl
+utils/cpanp
+utils/cpan2dist
utils/shasum
utils/splain
utils/xsubpp
diff --git a/utils/Makefile b/utils/Makefile
index a37a5708ff..3a96c9e8a0 100644
--- a/utils/Makefile
+++ b/utils/Makefile
@@ -5,9 +5,9 @@ REALPERL = ../perl
# Files to be built with variable substitution after miniperl is
# available. Dependencies handled manually below (for now).
-pl = c2ph.PL config_data.PL corelist.PL cpan.PL h2ph.PL h2xs.PL instmodsh.PL perlbug.PL perldoc.PL perlivp.PL pl2pm.PL prove.PL ptar.PL ptardiff.PL shasum.PL splain.PL dprofpp.PL libnetcfg.PL piconv.PL enc2xs.PL xsubpp.PL
-plextract = c2ph config_data corelist cpan h2ph h2xs instmodsh perlbug perldoc perlivp pl2pm prove ptar ptardiff shasum splain dprofpp libnetcfg piconv enc2xs xsubpp
-plextractexe = ./c2ph ./config_data ./corelist ./cpan ./h2ph ./h2xs ./instmodsh ./perlbug ./perldoc ./perlivp ./pl2pm ./prove ./ptar ./ptardiff ./shasum ./splain ./dprofpp ./libnetcfg ./piconv ./enc2xs ./xsubpp
+pl = c2ph.PL config_data.PL corelist.PL cpan.PL h2ph.PL h2xs.PL instmodsh.PL perlbug.PL perldoc.PL perlivp.PL pl2pm.PL prove.PL ptar.PL ptardiff.PL cpanp-run-perl.PL cpanp.PL cpan2dist.PL shasum.PL splain.PL dprofpp.PL libnetcfg.PL piconv.PL enc2xs.PL xsubpp.PL
+plextract = c2ph config_data corelist cpan h2ph h2xs instmodsh perlbug perldoc perlivp pl2pm prove ptar ptardiff cpanp-run-perl cpanp cpan2dist shasum splain dprofpp libnetcfg piconv enc2xs xsubpp
+plextractexe = ./c2ph ./config_data ./corelist ./cpan ./h2ph ./h2xs ./instmodsh ./perlbug ./perldoc ./perlivp ./pl2pm ./prove ./ptar ./ptardiff ./cpanp-run-perl ./cpanp ./cpan2dist ./shasum ./splain ./dprofpp ./libnetcfg ./piconv ./enc2xs ./xsubpp
all: $(plextract)
@@ -40,6 +40,12 @@ ptar: ptar.PL ../config.sh
ptardiff: ptardiff.PL ../config.sh
+cpanp-run-perl: cpanp-run-perl.PL ../config.sh
+
+cpanp: cpanp.PL ../config.sh
+
+cpan2dist: cpan2dist.PL ../config.sh
+
pl2pm: pl2pm.PL ../config.sh
shasum: shasum.PL ../config.sh
diff --git a/utils/cpan2dist.PL b/utils/cpan2dist.PL
new file mode 100644
index 0000000000..93a670989e
--- /dev/null
+++ b/utils/cpan2dist.PL
@@ -0,0 +1,51 @@
+#!/usr/local/bin/perl
+
+use Config;
+use File::Basename qw(&basename &dirname);
+use Cwd;
+
+# List explicitly here the variables you want Configure to
+# generate. Metaconfig only looks for shell variables, so you
+# have to mention them as if they were shell variables, not
+# %Config entries. Thus you write
+# $startperl
+# to ensure Configure will look for $Config{startperl}.
+
+# This forces PL files to create target in same directory as PL file.
+# This is so that make depend always knows where to find PL derivatives.
+my $origdir = cwd;
+chdir dirname($0);
+my $file = basename($0, '.PL');
+$file .= '.com' if $^O eq 'VMS';
+
+open OUT,">$file" or die "Can't create $file: $!";
+
+print "Extracting $file (with variable substitutions)\n";
+
+# In this section, perl variables will be expanded during extraction.
+# You can use $Config{...} to use Configure variables.
+
+print OUT <<"!GROK!THIS!";
+$Config{startperl}
+ eval 'exec $Config{perlpath} -S \$0 \${1+"\$@"}'
+ if \$running_under_some_shell;
+!GROK!THIS!
+
+use File::Spec;
+
+my $script = File::Spec->catfile(
+ File::Spec->catdir(
+ File::Spec->updir, qw[ lib CPANPLUS bin ]
+ ), "cpan2dist");
+
+if (open(IN, $script)) {
+ print OUT <IN>;
+ close IN;
+} else {
+ die "$0: cannot find '$script'\n";
+}
+
+close OUT or die "Can't close $file: $!";
+chmod 0755, $file or die "Can't reset permissions for $file: $!\n";
+exec("$Config{'eunicefix'} $file") if $Config{'eunicefix'} ne ':';
+chdir $origdir;
diff --git a/utils/cpanp-run-perl.PL b/utils/cpanp-run-perl.PL
new file mode 100644
index 0000000000..fe588f5173
--- /dev/null
+++ b/utils/cpanp-run-perl.PL
@@ -0,0 +1,51 @@
+#!/usr/local/bin/perl
+
+use Config;
+use File::Basename qw(&basename &dirname);
+use Cwd;
+
+# List explicitly here the variables you want Configure to
+# generate. Metaconfig only looks for shell variables, so you
+# have to mention them as if they were shell variables, not
+# %Config entries. Thus you write
+# $startperl
+# to ensure Configure will look for $Config{startperl}.
+
+# This forces PL files to create target in same directory as PL file.
+# This is so that make depend always knows where to find PL derivatives.
+my $origdir = cwd;
+chdir dirname($0);
+my $file = basename($0, '.PL');
+$file .= '.com' if $^O eq 'VMS';
+
+open OUT,">$file" or die "Can't create $file: $!";
+
+print "Extracting $file (with variable substitutions)\n";
+
+# In this section, perl variables will be expanded during extraction.
+# You can use $Config{...} to use Configure variables.
+
+print OUT <<"!GROK!THIS!";
+$Config{startperl}
+ eval 'exec $Config{perlpath} -S \$0 \${1+"\$@"}'
+ if \$running_under_some_shell;
+!GROK!THIS!
+
+use File::Spec;
+
+my $script = File::Spec->catfile(
+ File::Spec->catdir(
+ File::Spec->updir, qw[ lib CPANPLUS bin ]
+ ), "cpanp-run-perl");
+
+if (open(IN, $script)) {
+ print OUT <IN>;
+ close IN;
+} else {
+ die "$0: cannot find '$script'\n";
+}
+
+close OUT or die "Can't close $file: $!";
+chmod 0755, $file or die "Can't reset permissions for $file: $!\n";
+exec("$Config{'eunicefix'} $file") if $Config{'eunicefix'} ne ':';
+chdir $origdir;
diff --git a/utils/cpanp.PL b/utils/cpanp.PL
new file mode 100644
index 0000000000..932337b711
--- /dev/null
+++ b/utils/cpanp.PL
@@ -0,0 +1,51 @@
+#!/usr/local/bin/perl
+
+use Config;
+use File::Basename qw(&basename &dirname);
+use Cwd;
+
+# List explicitly here the variables you want Configure to
+# generate. Metaconfig only looks for shell variables, so you
+# have to mention them as if they were shell variables, not
+# %Config entries. Thus you write
+# $startperl
+# to ensure Configure will look for $Config{startperl}.
+
+# This forces PL files to create target in same directory as PL file.
+# This is so that make depend always knows where to find PL derivatives.
+my $origdir = cwd;
+chdir dirname($0);
+my $file = basename($0, '.PL');
+$file .= '.com' if $^O eq 'VMS';
+
+open OUT,">$file" or die "Can't create $file: $!";
+
+print "Extracting $file (with variable substitutions)\n";
+
+# In this section, perl variables will be expanded during extraction.
+# You can use $Config{...} to use Configure variables.
+
+print OUT <<"!GROK!THIS!";
+$Config{startperl}
+ eval 'exec $Config{perlpath} -S \$0 \${1+"\$@"}'
+ if \$running_under_some_shell;
+!GROK!THIS!
+
+use File::Spec;
+
+my $script = File::Spec->catfile(
+ File::Spec->catdir(
+ File::Spec->updir, qw[ lib CPANPLUS bin ]
+ ), "cpanp");
+
+if (open(IN, $script)) {
+ print OUT <IN>;
+ close IN;
+} else {
+ die "$0: cannot find '$script'\n";
+}
+
+close OUT or die "Can't close $file: $!";
+chmod 0755, $file or die "Can't reset permissions for $file: $!\n";
+exec("$Config{'eunicefix'} $file") if $Config{'eunicefix'} ne ':';
+chdir $origdir;
diff --git a/win32/Makefile b/win32/Makefile
index 1de959fbaf..e1f15a48d7 100644
--- a/win32/Makefile
+++ b/win32/Makefile
@@ -593,6 +593,9 @@ UTILS = \
..\utils\prove \
..\utils\ptar \
..\utils\ptardiff \
+ ..\utils\cpanp-run-perl \
+ ..\utils\cpanp \
+ ..\utils\cpan2dist \
..\utils\shasum \
..\utils\instmodsh \
..\pod\checkpods \
@@ -1160,7 +1163,7 @@ distclean: realclean
podchecker podselect
-cd ..\utils && del /f h2ph splain perlbug pl2pm c2ph pstruct h2xs \
perldoc perlivp dprofpp libnetcfg enc2xs piconv cpan *.bat \
- xsubpp instmodsh prove ptar ptardiff shasum corelist config_data
+ xsubpp instmodsh prove ptar ptardiff cpanp-run-perl cpanp cpan2dist shasum corelist config_data
-cd ..\x2p && del /f find2perl s2p psed *.bat
-del /f ..\config.sh ..\splittree.pl perlmain.c dlutils.c config.h.new \
perlmainst.c
diff --git a/win32/makefile.mk b/win32/makefile.mk
index e5f6bc6f94..966aa7478b 100644
--- a/win32/makefile.mk
+++ b/win32/makefile.mk
@@ -746,6 +746,9 @@ UTILS = \
..\utils\prove \
..\utils\ptar \
..\utils\ptardiff \
+ ..\utils\cpanp-run-perl \
+ ..\utils\cpanp \
+ ..\utils\cpan2dist \
..\utils\shasum \
..\utils\instmodsh \
..\pod\checkpods \
@@ -1487,7 +1490,7 @@ distclean: realclean
podchecker podselect
-cd ..\utils && del /f h2ph splain perlbug pl2pm c2ph pstruct h2xs \
perldoc perlivp dprofpp libnetcfg enc2xs piconv cpan *.bat \
- xsubpp instmodsh prove ptar ptardiff shasum corelist config_data
+ xsubpp instmodsh prove ptar ptardiff cpanp-run-perl cpanp cpan2dist shasum corelist config_data
-cd ..\x2p && del /f find2perl s2p psed *.bat
-del /f ..\config.sh ..\splittree.pl perlmain.c dlutils.c config.h.new \
perlmainst.c