From 9bd5fe89fce36e0c509089346a4f3af885097251 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Fri, 8 Jul 2011 16:37:29 -0700 Subject: [PATCH 01/12] Allow add() method to accept multiple modules. The add() method can now accept a "modules" (plural) argument that is a hasref containing ModuleName => ModuleVersion pairs. This can be used as an alternative to (but not in conjunction with) the "module" (singular) and version arguments. In practice, any non-trivial distribution is going to contain multiple modules. And those modules could all have different version numbers. But the old "module" (singular) and "version" interface did not support that reality. In fact, mcpi[1] worked around this limitation by calling add() for each module in the distribution. Not only did this seem odd, it causes the tar.gz file to be needlessly copied several times. I would even sugges that the old "module" (singular) and "version" interface sould be deprecated. I added test cases to cover the new interface. Also added test cases to cover the argument validation logic for the add() method. And I updated the POD accordingly. --- lib/CPAN/Mini/Inject.pm | 111 ++++++++++++++++++++++++++-------------- t/add.t | 42 ++++++++++++--- 2 files changed, 108 insertions(+), 45 deletions(-) diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index 738e027..9b814f4 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -41,7 +41,13 @@ probably want to look at the mcpani command instead. $mcpi->add( module => 'CPAN::Mini::Inject', authorid => 'SSORICHE', version => ' 0.01', - file => 'mymodules/CPAN-Mini-Inject-0.01.tar.gz' ) + file => 'mymodules/CPAN-Mini-Inject-0.01.tar.gz' ); + + # or... + + $mcpi->add( modules => { Foo::Bar => '0.01', Foo::Baz => '0.03' }, + authorid => 'SSORICHE', + file => 'mymodules/Distro-With-Many-Modules-1.2.tar.gz' ); $mcpi->writelist; $mcpi->update_mirror; @@ -250,75 +256,94 @@ structure below the repository. The name of the module to add. -=item * authorid - -CPAN author id. This does not have to be a real author id. - =item * version The modules version number. +=item * modules + +A reference to a hash of ModuleName => ModuleVersion pairs. This can +be used in place of the single C amd arguments when +you have a distribution that contains multiple modules. + +=item * authorid + +CPAN author id. This does not have to be a real author id. + =item * file The tar.gz of the module. =back -=head3 Example +=head3 Example, with a distribution that contains only one module: add( module => 'Module::Name', authorid => 'AUTHOR', version => 0.01, file => './Module-Name-0.01.tar.gz' ); +=head3 Example, with a distribution that contains multiple modules: + + add( modules => { + Animal => '0.1', + Animal::Bear => '1.2', + Animal::Zebra => '2.6', + }, + authorid => 'AUTHOR', + file => './Zoo-3.2.tar.gz' ); + =cut sub add { my $self = shift; my %options = @_; - my $optionchk - = _optionchk( \%options, qw/module authorid version file/ ); + _add_optionchk( %options ); # Croaks if invalid! + + my $modulepath = $options{file}; + my $modulefile = basename $options{file}; + my $authorid = uc $options{authorid}; + my $modules = $options{modules} || { $options{module} => $options{version} }; + my $repository = $self->config->get( 'repository' ); - croak "Required option not specified: $optionchk" if $optionchk; croak "No repository configured" - unless ( $self->config->get( 'repository' ) ); - croak "Can not write to repository: " - . $self->config->get( 'repository' ) - unless ( -w $self->config->get( 'repository' ) ); + unless ( $repository ); + + croak "Can not write to repository: $repository" + unless ( -w $repository ); croak "Can not read module file: $options{file}" - unless -r $options{file}; + unless -r $options{file}; - my $modulefile = basename( $options{file} ); $self->readlist unless exists( $self->{modulelist} ); - $options{authorid} = uc( $options{authorid} ); - $self->{authdir} = $self->_authordir( $options{authorid}, - $self->config->get( 'repository' ) ); + $self->{authdir} = $self->_authordir( $authorid, $repository ); my $target - = $self->config->get( 'repository' ) + = $repository . '/authors/id/' . $self->{authdir} . '/' - . basename( $options{file} ); + . $modulefile; - copy( $options{file}, dirname( $target ) ) + copy( $modulepath, dirname( $target ) ) or croak "Copy failed: $!"; $self->_updperms( $target ); - # remove old version from the list - @{ $self->{modulelist} } - = grep { $_ !~ m/\A$options{module}\s+/ } @{ $self->{modulelist} }; + while (my ($modulename, $version) = each %$modules) { - push( - @{ $self->{modulelist} }, - _fmtmodule( - $options{module}, File::Spec::Unix->catfile( File::Spec->splitdir( $self->{authdir} ), $modulefile ), - $options{version} - ) - ); + # remove old version from the list + @{ $self->{modulelist} } + = grep { $_ !~ m/\A$modulename\s+/ } @{ $self->{modulelist} }; + + push( + @{ $self->{modulelist} }, + _fmtmodule($modulename, + File::Spec::Unix->catfile(File::Spec->splitdir( $self->{authdir} ), $modulefile ), + $version ) + ); + } return $self; } @@ -504,16 +529,24 @@ sub _updperms { if $self->config->get( 'dirmode' ); } -sub _optionchk { - my ( $options, @list ) = @_; - my @missing; +sub _add_optionchk { + my ( %options ) = @_; - for my $option ( @list ) { - push @missing, $option - unless defined $$options{$option}; - } + my @missing_options = grep { not $options{$_} } qw(authorid file); + croak "Required option not specified: " . join ' ', @missing_options + if @missing_options; + + my ($mod, $ver, $mods) = @options{qw(module version modules)}; + croak "Must specify either 'modules' or ('module' and 'version')" + unless ( ($mod and $ver) or $mods); + + croak "The modules argument must be a hashref" + if $mods and ref $mods ne 'HASH'; + + my @no_version = grep { not defined $mods->{$_} } keys %$mods; + croak "Version number required for modules: ", join ' ', @no_version + if @no_version; - return join ' ', @missing; } sub _make_path { diff --git a/t/add.t b/t/add.t index 1e52bfb..f049805 100644 --- a/t/add.t +++ b/t/add.t @@ -1,4 +1,5 @@ -use Test::More tests => 6; +use Test::More tests => 14; +use Test::Exception; use CPAN::Mini::Inject; use File::Path; @@ -19,7 +20,11 @@ $mcpi->add( authorid => 'SSORICHE', version => '0.02', file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' - ); + )->add( + modules => {Foo => '1.0', Bar => '2.0'}, + authorid => 'SSORICHE', + file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' +); my $soriche_path = File::Spec->catfile( 'S', 'SS', 'SSORICHE' ); is( $mcpi->{authdir}, $soriche_path, 'author directory' ); @@ -27,13 +32,38 @@ ok( -r 't/local/MYCPAN/authors/id/S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', 'Added module is readable' ); -my $module - = "CPAN::Mini::Inject 0.02 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz"; -ok( grep( /$module/, @{ $mcpi->{modulelist} } ), - 'Module added to list' ); +my @modules = ( + 'CPAN::Mini::Inject 0.02 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', + 'Foo 1.0 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', + 'Bar 2.0 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', +); + +for my $module (@modules) { + ok( grep( /$module/, @{ $mcpi->{modulelist} } ), "Module added to list" ); +} + is( grep( /^CPAN::Mini::Inject\s+/, @{ $mcpi->{modulelist} } ), 1, 'Module added to list just once' ); +# Test argument validation on add() method +dies_ok {$mcpi->add( authorid => 'AUTHOR', modules => {}) } + 'Missing file argument'; + +dies_ok {$mcpi->add( file => 'My-Modules-1.0.tar.gz', modules => {}) } + 'Missing authorid argument'; + +dies_ok {$mcpi->add( authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + 'Missing both module and modules argument'; + +dies_ok {$mcpi->add( modules => {}, module => 'FOO', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + 'Both module and modules argument given'; + +dies_ok {$mcpi->add( modules => {}, version => '1.0', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + 'Both modules and version argument given'; + +dies_ok {$mcpi->add( modules => {Foo => undef}, authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + 'Missing version number for a module'; + SKIP: { skip "Not a UNIX system", 2 if ( $^O =~ /^MSWin/ ); is( ( stat( 't/local/MYCPAN/authors/id/S/SS/SSORICHE' ) )[2] & 07777, From be29a4993d05bc40baf7ed1807a596b77158fb15 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 10:56:43 -0700 Subject: [PATCH 02/12] Fixing option validation logic for add() method. --- lib/CPAN/Mini/Inject.pm | 3 ++- t/add.t | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index 9b814f4..49649c3 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -537,8 +537,9 @@ sub _add_optionchk { if @missing_options; my ($mod, $ver, $mods) = @options{qw(module version modules)}; + croak "Must specify either 'modules' or ('module' and 'version')" - unless ( ($mod and $ver) or $mods); + unless ( ($mod and $ver) or ($mods and not ($mod or $ver)) ); croak "The modules argument must be a hashref" if $mods and ref $mods ne 'HASH'; diff --git a/t/add.t b/t/add.t index f049805..e31a303 100644 --- a/t/add.t +++ b/t/add.t @@ -39,30 +39,30 @@ my @modules = ( ); for my $module (@modules) { - ok( grep( /$module/, @{ $mcpi->{modulelist} } ), "Module added to list" ); + ok( grep( /$module/, @{ $mcpi->{modulelist} } ), "Module $module added to list" ); } is( grep( /^CPAN::Mini::Inject\s+/, @{ $mcpi->{modulelist} } ), 1, 'Module added to list just once' ); # Test argument validation on add() method -dies_ok {$mcpi->add( authorid => 'AUTHOR', modules => {}) } - 'Missing file argument'; +throws_ok {$mcpi->add( authorid => 'AUTHOR', modules => {}) } + qr/Required option not specified: file/, 'Missing file argument'; -dies_ok {$mcpi->add( file => 'My-Modules-1.0.tar.gz', modules => {}) } - 'Missing authorid argument'; +throws_ok {$mcpi->add( file => 'My-Modules-1.0.tar.gz', modules => {}) } + qr/Required option not specified: authorid/, 'Missing authorid argument'; -dies_ok {$mcpi->add( authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - 'Missing both module and modules argument'; +throws_ok {$mcpi->add( authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/Must specify either/, 'Missing module and modules argument',; -dies_ok {$mcpi->add( modules => {}, module => 'FOO', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - 'Both module and modules argument given'; +throws_ok {$mcpi->add( modules => {}, module => 'FOO', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/Must specify either/, 'Both module and modules argument given'; -dies_ok {$mcpi->add( modules => {}, version => '1.0', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - 'Both modules and version argument given'; +throws_ok {$mcpi->add( modules => {}, version => '1.0', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/Must specify either/, 'Both modules and version argument given'; -dies_ok {$mcpi->add( modules => {Foo => undef}, authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - 'Missing version number for a module'; +throws_ok {$mcpi->add( modules => {Foo => undef}, authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/Version number required for modules: Foo/, 'Missing version number for a module'; SKIP: { skip "Not a UNIX system", 2 if ( $^O =~ /^MSWin/ ); From bd9096246c33da118b6368b4359f941d3bc1b4e1 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 11:00:31 -0700 Subject: [PATCH 03/12] Go back to old name for _optionchk() I decided to just keep the old name for this subroutine, just to minimize churn. Although this sub is only useful for the add() method. --- lib/CPAN/Mini/Inject.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index 49649c3..ada8883 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -299,7 +299,7 @@ sub add { my $self = shift; my %options = @_; - _add_optionchk( %options ); # Croaks if invalid! + _optionchk( %options ); # Croaks if invalid! my $modulepath = $options{file}; my $modulefile = basename $options{file}; @@ -529,7 +529,7 @@ sub _updperms { if $self->config->get( 'dirmode' ); } -sub _add_optionchk { +sub _optionchk { my ( %options ) = @_; my @missing_options = grep { not $options{$_} } qw(authorid file); From 5556e8206a99ef1814a67f2fd555a59f3c160d88 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 11:02:51 -0700 Subject: [PATCH 04/12] Add MYMETA.* to the ignore list. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 20d83ab..0e8719c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ pm_to_blib* *.swp *.bak t/local/WRITEREPO +MYMETA.* From 8ab6489051347855c710ea295bd18bd98905c4e4 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 12:05:39 -0700 Subject: [PATCH 05/12] Modified POD to reflect my preferred interface. Now I just have to make the code actually behave that way. --- bin/mcpani | 148 ++++++++++++++++++++++++++++------------------------- 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/bin/mcpani b/bin/mcpani index 7d80328..560d243 100644 --- a/bin/mcpani +++ b/bin/mcpani @@ -43,7 +43,7 @@ sub add { $mcpi->readlist; - my @modules_to_add; + my %modules_to_add; if ( $options{'all-in-meta'} ) { ## attempt to read the META.yml my $meta_data = _load_meta( $options{file} ); @@ -63,11 +63,7 @@ sub add { ? $info->{version} : 'undef'; - push @modules_to_add, - { - version => $v, - module => $name - }; + $modules_to_add{$name} = $v; } } @@ -78,39 +74,31 @@ sub add { my $provides = _find_provides( $options{file} ); while ( my ( $module, $version ) = each( %{$provides} ) ) { - push @modules_to_add, - { - version => $version, - module => $module - }; + $modules_to_add{$module} = $version; } } - # always add the module/version provided on the command line - # as well - push @modules_to_add, - { - version => $options{version}, - module => $options{module}, - }; + # always add the module/version provided on the command line as well + $modules_to_add{$options{module}} = $options{version}; $mcpi->readlist; - for my $item ( @modules_to_add ) { - $mcpi->add( - module => $item->{module}, - authorid => $options{authorid}, - version => $item->{version}, - file => $options{file} - ); - - if ( $options{verbose} ) { - print "\nAdding module: $item->{module}\n"; - print "Author ID: $options{authorid}\n"; - print "Version: $item->{version}\n"; - print "File: $options{file}\n"; - print "To repository: $mcpi->{config}{repository}\n\n"; + $mcpi->add( + modules => \%modules_to_add, + authorid => $options{authorid}, + file => $options{file} + ); + + if ( $options{verbose} ) { + local $, = ' '; + my @modules = sort keys %modules_to_add; + my @versions = @modules_to_add{@modules}; + print "\nAdding module(s): @modules\n"; + print "Author ID: $options{authorid}\n"; + print "Version(s): @versions\n"; + print "File: $options{file}\n"; + print "To repository: $mcpi->{config}{repository}\n\n"; } - } + $mcpi->writelist; } @@ -234,9 +222,8 @@ GetOptions( 'update' => sub { setsub( 'update', \&update ) }, 'mirror' => sub { setsub( 'mirror', \&mirror ) }, 'inject' => sub { setsub( 'inject', \&inject ) }, - 'module=s' => \$options{module}, + 'module:s%' => \$options{module}, 'authorid=s' => \$options{authorid}, - 'modversion=s' => \$options{version}, 'file=s' => \$options{file}, 'all-in-meta' => \$options{'all-in-meta'}, 'signing-key=s' => \$options{'signing_key'}, @@ -267,29 +254,30 @@ mcpani [options] < --add | --update | --mirror | --inject > Commands: - --add Add a new package to the repository - --module Name of the module to add - --authorid Author ID of the module - --modversion Version number of the module - --all-in-meta parse all modules in the META.yml - --discover-packages discover modules in all .pm files - --file tar.gz file of the module + --add Add a new module to the repository - --update Update local CPAN mirror and inject modules - --mirror Update local CPAN mirror from remote - --inject Add modules from repository to CPAN mirror + --module NAME=VERSION Explicit NAME and VERSION of the module to add (repeatable) + --modversion VERSION Default VERSION for all modules + --authorid ID Author ID of the module + --all-in-meta Get module names and versions from the META.yml + --discover-packages Discover modules from all the .pm files + --file FILE path to tar.gz file containing the module(s) + + --update Update local CPAN mirror and inject modules + --mirror Update local CPAN mirror from remote + --inject Add modules from repository to CPAN mirror Options: - -h, --help This synopsis - -H, --man Detailed description + -h, --help This synopsis + -H, --man Detailed description - -l, --local local location for CPAN::Mini Mirror - -r, --remote CPAN mirror to mirror from - -p, --passive Enable passive ftp for mirroring. - -v, --verbose verbose output - -V, --version Version information. - --signing-key See CPAN::Checksums $SIGNING_KEY + -l, --local local location for CPAN::Mini Mirror + -r, --remote CPAN mirror to mirror from + -p, --passive Enable passive ftp for mirroring. + -v, --verbose Verbose output + -V, --version Version information. + --signing-key See CPAN::Checksums $SIGNING_KEY =head1 COMMAND LINE OPTIONS @@ -300,31 +288,44 @@ mirror. The add command requires the following parameters: =over 4 -=item --module +=item --module NAME=VERSION + +=item --module NAME -This is the name of the module (ie CPAN::Mini::Inject). +This is the NAME and VERSION of the module to add. This option can be +repeated for multiple options. If you do not specify a VERSION here, +it will be taken from the C<--modversion> option. + +=item --modversion VERSION + +This is the default VERSION for all modules. This VERSION will be +used for any module that was not explicity given a VERSION in the +C<--module> option. =item --authorid A CPAN 'like' author ID for the module. The author ID does not need to exist on CPAN. -=item --modversion - -Version number of the module. This must match the version number in the -file name. - =item --all-in-meta This option will add every module listed in the 'provides' section of -the META.yml contained in the tar.gz provided by the --file option. +the F contained in the tar.gz provided by the --file option. + +The options C<--module> and C<--modversion> are still recognized, and +will override the values in the F file. + +If the F file or the 'provides' section is missing, then a +warning is issued and the only modules added are those provided by +C<--module> option(s). + +=item --discover-packages -The options --module and --modversion are still recognized. If the -same module/version is found in the META.yml it is not duplicated. +This option will attempt to discover the modules in your tar.gz +by examining all the F<.pm> files in the F directory. -If the META.yml file or the 'provides' section is missing, then -a warning is issued and the only module added is the one provided by ---module / --modversion. +The options C<--module> and C<--modversion> are still recognized, and +will override the values discovered in the F<.pm> files. =item --file @@ -334,11 +335,20 @@ C). =back - Example: +Example with a single module: + + mcpani --add --module Romeo --modversion 1.2 --authorid THEBARD + --file ./Romeo-Juliet-1.2.tar.gz + +Example with multiple modules, having different versions: + + mcpani --add --module Montague=1.2 --module Capulet=2.4 --author THEBARD + --file ./Romeo-Juliet-3.2.tar.gz - mcpani --add --module CPAN::Mini::Inject --authorid SSORICHE - --modversion 0.01 --file ./CPAN-Mini-Inject-0.01.tar.gz +Example with muliple modules, using a default version for all: + mcpani --add --module Montague --module Capulet --modversion 3.2 --author THEBARD + --file ./Romeo-Juliet-3.2.tar.gz =head2 --update From e3b50acfb877fed50878a0e50e1e5c7d11f49279 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 14:14:15 -0700 Subject: [PATCH 06/12] Improvements on the add() interface. You may now specify the version argument with the modules (plural) argument. This serves as the default version of any modules that you did not specify an explicit version number for. This is convenient for module authors (like me) who assign the same version number to all modules in a particular release. --- lib/CPAN/Mini/Inject.pm | 29 +++++++++++++++++++---------- t/add.t | 24 +++++++++++++++++------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index ada8883..33c8ba7 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -304,7 +304,8 @@ sub add { my $modulepath = $options{file}; my $modulefile = basename $options{file}; my $authorid = uc $options{authorid}; - my $modules = $options{modules} || { $options{module} => $options{version} }; + my $defaultversion = defined $options{version} ? $options{version} : 'undef'; + my $modules = $options{modules} || { $options{module} => $defaultversion }; my $repository = $self->config->get( 'repository' ); croak "No repository configured" @@ -331,7 +332,12 @@ sub add { $self->_updperms( $target ); - while (my ($modulename, $version) = each %$modules) { + foreach my $modulename (sort keys %$modules) { + + # Use default version if none given explicitly for this module + # Remember that versions can be zero too. + my $moduleversion = defined $modules->{$modulename} ? + $modules->{$modulename} : $defaultversion; # remove old version from the list @{ $self->{modulelist} } @@ -341,7 +347,7 @@ sub add { @{ $self->{modulelist} }, _fmtmodule($modulename, File::Spec::Unix->catfile(File::Spec->splitdir( $self->{authdir} ), $modulefile ), - $version ) + $moduleversion ) ); } @@ -532,22 +538,25 @@ sub _updperms { sub _optionchk { my ( %options ) = @_; + # TODO: Don't call them "options" if they are required! + my @missing_options = grep { not $options{$_} } qw(authorid file); croak "Required option not specified: " . join ' ', @missing_options if @missing_options; my ($mod, $ver, $mods) = @options{qw(module version modules)}; - croak "Must specify either 'modules' or ('module' and 'version')" - unless ( ($mod and $ver) or ($mods and not ($mod or $ver)) ); + croak "Must specify either 'modules' or 'module')" + unless ( $mod xor $mods ); - croak "The modules argument must be a hashref" - if $mods and ref $mods ne 'HASH'; + croak "Must specify 'version' with 'module')" + if ( $mod and not $ver ); - my @no_version = grep { not defined $mods->{$_} } keys %$mods; - croak "Version number required for modules: ", join ' ', @no_version - if @no_version; + croak "The 'modules' argument must be a hashref" + if ($mods and ref $mods ne 'HASH'); + croak "Must specify 'version' if 'modules' does not contain a version for each module" + if ( not $ver and grep { not $mods->{$_} } keys %$mods ); } sub _make_path { diff --git a/t/add.t b/t/add.t index e31a303..b95b7d4 100644 --- a/t/add.t +++ b/t/add.t @@ -1,4 +1,4 @@ -use Test::More tests => 14; +use Test::More tests => 17; use Test::Exception; use CPAN::Mini::Inject; @@ -21,9 +21,16 @@ $mcpi->add( version => '0.02', file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' )->add( + # Injecting multiple modules, with different versions modules => {Foo => '1.0', Bar => '2.0'}, authorid => 'SSORICHE', file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' +)->add( + # Injecting multiple modules, with different versions, and a default version + modules => {Fred => '0.0', Wilma => undef, Barney => '1.2'}, + authorid => 'SSORICHE', + version => '4.0', + file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' ); my $soriche_path = File::Spec->catfile( 'S', 'SS', 'SSORICHE' ); @@ -34,12 +41,15 @@ ok( ); my @modules = ( 'CPAN::Mini::Inject 0.02 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', - 'Foo 1.0 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', 'Bar 2.0 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', + 'Foo 1.0 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', + 'Barney 1.2 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', + 'Fred 0.0 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', + 'Wilma 4.0 S/SS/SSORICHE/CPAN-Mini-Inject-0.01.tar.gz', ); for my $module (@modules) { - ok( grep( /$module/, @{ $mcpi->{modulelist} } ), "Module $module added to list" ); + ok( grep( /$module/, @{ $mcpi->{modulelist} } ), "Added: $module" ); } is( grep( /^CPAN::Mini::Inject\s+/, @{ $mcpi->{modulelist} } ), @@ -53,16 +63,16 @@ throws_ok {$mcpi->add( file => 'My-Modules-1.0.tar.gz', modules => {}) } qr/Required option not specified: authorid/, 'Missing authorid argument'; throws_ok {$mcpi->add( authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - qr/Must specify either/, 'Missing module and modules argument',; + qr/Must specify either/, 'Missing module and modules argument'; throws_ok {$mcpi->add( modules => {}, module => 'FOO', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } qr/Must specify either/, 'Both module and modules argument given'; -throws_ok {$mcpi->add( modules => {}, version => '1.0', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - qr/Must specify either/, 'Both modules and version argument given'; +throws_ok {$mcpi->add( modules => 'string', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/must be a hashref/, 'The modules argument is not a hashref'; throws_ok {$mcpi->add( modules => {Foo => undef}, authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - qr/Version number required for modules: Foo/, 'Missing version number for a module'; + qr/Must specify 'version'/, 'No default version, when explicit version is not given'; SKIP: { skip "Not a UNIX system", 2 if ( $^O =~ /^MSWin/ ); From 27a71b1d894c0e341f3e6ecc8f3efcc88e805058 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 14:16:08 -0700 Subject: [PATCH 07/12] The --module option is now awesome. The --module option for mcpi[1] now takes MODULE_NAME=MODULE_VERSION arguments and can be repeated for multiple modules. If you do not specify a MODULE_VERSION for any given MODULE_NAME, then the value of the --version option is used as the default. Here are some examples: # Adds Foo-1.2 and Bar-2.3 mcpi --add --module Foo=1.2 Bar=2.3 --author ME --file Distro.tar.gz # Adds Foo-1.2 and Bar-4.1 mcpi --add --module Foo=1.2 Bar --version 4.1 --author ME --fle Distro.tar.gz # Adds Foo-5.2 and Bar-5.2 mcpi --add --module Foo --module Bar --version 5.2 --author ME --file Distro.tar.gz Notice that this is still perfectly compatible with the old style of using --module and --version to add a single module. --- bin/mcpani | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/mcpani b/bin/mcpani index 560d243..cf370f4 100644 --- a/bin/mcpani +++ b/bin/mcpani @@ -79,11 +79,12 @@ sub add { } # always add the module/version provided on the command line as well - $modules_to_add{$options{module}} = $options{version}; + $modules_to_add{$options{module}} = $options{version} if $options{module}; $mcpi->readlist; $mcpi->add( modules => \%modules_to_add, + version => $options{version}, authorid => $options{authorid}, file => $options{file} ); From 5af9d18b4f919d9045093989a2e23dc1ef0e9926 Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 14:26:47 -0700 Subject: [PATCH 08/12] POD edits. --- bin/mcpani | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/mcpani b/bin/mcpani index cf370f4..388a380 100644 --- a/bin/mcpani +++ b/bin/mcpani @@ -294,7 +294,7 @@ mirror. The add command requires the following parameters: =item --module NAME This is the NAME and VERSION of the module to add. This option can be -repeated for multiple options. If you do not specify a VERSION here, +repeated for multiple modules. If you do not specify a VERSION here, it will be taken from the C<--modversion> option. =item --modversion VERSION From b108e9b64b8b5cb5c99753d95d43e4f80a6848fd Mon Sep 17 00:00:00 2001 From: Jeffrey Thalhammer Date: Mon, 11 Jul 2011 15:08:48 -0700 Subject: [PATCH 09/12] The module argument to add() is now polymorphic. Instead of having separate module (singular) and modules (plural) arguments, the module argument is now polymorphic, and will accept either a stirng (for one module) or a hashref (for multiple modules). --- lib/CPAN/Mini/Inject.pm | 78 +++++++++++++++++++++++++++-------------- t/add.t | 22 ++++++------ 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index 33c8ba7..6cd5c6d 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -254,17 +254,21 @@ structure below the repository. =item * module -The name of the module to add. +Either the name of a single module as a string (e.g 'Foo::Bar') or a +reference to a hash of NAME => VERSION pairs (e.g. {Foo::Bar => '1.2', +Foo::Baz => '2.3'}). In the former case, the C argument is +always required (see below). In the latter case, the version numbers +are optional and the default is taken from the C argument +(see below). =item * version -The modules version number. - -=item * modules - -A reference to a hash of ModuleName => ModuleVersion pairs. This can -be used in place of the single C amd arguments when -you have a distribution that contains multiple modules. +The default version number of all added modules. This is always +required when the C argument is just the name of a single +module. When the C argument is a hashref of NAME => VERSION +pairs, the C arguemnt is optional and is used as the default +version for all modules that don't have a defined VERSION in that +hashref. =item * authorid @@ -278,20 +282,31 @@ The tar.gz of the module. =head3 Example, with a distribution that contains only one module: - add( module => 'Module::Name', + add( module => 'Module::Name', authorid => 'AUTHOR', - version => 0.01, - file => './Module-Name-0.01.tar.gz' ); + version => 0.01, + file => './Module-Name-0.01.tar.gz' ); -=head3 Example, with a distribution that contains multiple modules: +=head3 Example, with a distribution that contains multiple modules with different versions: - add( modules => { + add( module => { Animal => '0.1', Animal::Bear => '1.2', Animal::Zebra => '2.6', }, authorid => 'AUTHOR', - file => './Zoo-3.2.tar.gz' ); + file => './Zoo-3.2.tar.gz' ); + +=head3 Example, with a distribution that contains multiple modules, all with the same version: + + add( module => { + Animal => undef, + Animal::Bear => undef, + Animal::Zebra => undef, + }, + authorid => 'AUTHOR', + version => '2.3', + file => './Zoo-3.2.tar.gz' ); =cut @@ -304,8 +319,13 @@ sub add { my $modulepath = $options{file}; my $modulefile = basename $options{file}; my $authorid = uc $options{authorid}; - my $defaultversion = defined $options{version} ? $options{version} : 'undef'; - my $modules = $options{modules} || { $options{module} => $defaultversion }; + + my $defaultversion = defined $options{version} ? + $options{version} : 'undef'; + + my $modules = ref $options{module} eq 'HASH' ? + $options{module} : { $options{module} => $defaultversion }; + my $repository = $self->config->get( 'repository' ); croak "No repository configured" @@ -540,23 +560,29 @@ sub _optionchk { # TODO: Don't call them "options" if they are required! - my @missing_options = grep { not $options{$_} } qw(authorid file); + my @missing_options = grep { not $options{$_} } qw(authorid file module); croak "Required option not specified: " . join ' ', @missing_options if @missing_options; - my ($mod, $ver, $mods) = @options{qw(module version modules)}; + my ($mod, $ver) = @options{qw(module version)}; + - croak "Must specify either 'modules' or 'module')" - unless ( $mod xor $mods ); - croak "Must specify 'version' with 'module')" - if ( $mod and not $ver ); + if (ref $mod eq '') { + + croak "The 'version' argument must be given when 'module' is a string" + if not $ver; + } + elsif (ref $mod eq 'HASH') { - croak "The 'modules' argument must be a hashref" - if ($mods and ref $mods ne 'HASH'); + croak "Must specify 'version' if 'module' does not contain a version for each module" + if ( not $ver and grep { not $mod->{$_} } keys %$mod ); + } + else { + + croak "The 'module' argument must be a string or hashref"; + } - croak "Must specify 'version' if 'modules' does not contain a version for each module" - if ( not $ver and grep { not $mods->{$_} } keys %$mods ); } sub _make_path { diff --git a/t/add.t b/t/add.t index b95b7d4..09d6ada 100644 --- a/t/add.t +++ b/t/add.t @@ -22,12 +22,12 @@ $mcpi->add( file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' )->add( # Injecting multiple modules, with different versions - modules => {Foo => '1.0', Bar => '2.0'}, + module => {Foo => '1.0', Bar => '2.0'}, authorid => 'SSORICHE', file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' )->add( # Injecting multiple modules, with different versions, and a default version - modules => {Fred => '0.0', Wilma => undef, Barney => '1.2'}, + module => {Fred => '0.0', Wilma => undef, Barney => '1.2'}, authorid => 'SSORICHE', version => '4.0', file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' @@ -56,23 +56,23 @@ is( grep( /^CPAN::Mini::Inject\s+/, @{ $mcpi->{modulelist} } ), 1, 'Module added to list just once' ); # Test argument validation on add() method -throws_ok {$mcpi->add( authorid => 'AUTHOR', modules => {}) } +throws_ok {$mcpi->add( authorid => 'AUTHOR', module => {}) } qr/Required option not specified: file/, 'Missing file argument'; -throws_ok {$mcpi->add( file => 'My-Modules-1.0.tar.gz', modules => {}) } +throws_ok {$mcpi->add( file => 'My-Modules-1.0.tar.gz', module => {}) } qr/Required option not specified: authorid/, 'Missing authorid argument'; throws_ok {$mcpi->add( authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - qr/Must specify either/, 'Missing module and modules argument'; + qr/Required option not specified: module/, 'Missing module argument'; -throws_ok {$mcpi->add( modules => {}, module => 'FOO', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - qr/Must specify either/, 'Both module and modules argument given'; +throws_ok {$mcpi->add( module => 'MyModule', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/The 'version' argument must be given/, 'No default version, when module is a single string'; -throws_ok {$mcpi->add( modules => 'string', authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - qr/must be a hashref/, 'The modules argument is not a hashref'; +throws_ok {$mcpi->add( module => [], authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/must be a string or hashref/, 'The module argument is wrong type'; -throws_ok {$mcpi->add( modules => {Foo => undef}, authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } - qr/Must specify 'version'/, 'No default version, when explicit version is not given'; +throws_ok {$mcpi->add( module => {Foo => undef}, authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } + qr/Must specify 'version'/, 'No default version and no explicit version either'; SKIP: { skip "Not a UNIX system", 2 if ( $^O =~ /^MSWin/ ); From 23d0f4dbdd6c3576cb457f3190d51ad0f0d67ee8 Mon Sep 17 00:00:00 2001 From: Tim Bunce Date: Thu, 11 Aug 2011 08:07:36 -0600 Subject: [PATCH 10/12] Allow file argument to be a URL --- lib/CPAN/Mini/Inject.pm | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index 6cd5c6d..c2a14b1 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -14,6 +14,7 @@ use File::Copy; use File::Path qw( make_path ); use File::Spec; use LWP::Simple; +use URI; =head1 NAME @@ -276,7 +277,7 @@ CPAN author id. This does not have to be a real author id. =item * file -The tar.gz of the module. +The tar.gz of the module. Can also be a URL. =back @@ -316,8 +317,8 @@ sub add { _optionchk( %options ); # Croaks if invalid! - my $modulepath = $options{file}; - my $modulefile = basename $options{file}; + my $file_uri = URI->new($options{file} =~ m/^\w+:/ ? $options{file} : "file:$options{file}"); + my $modulefile = basename $file_uri->path; my $authorid = uc $options{authorid}; my $defaultversion = defined $options{version} ? @@ -347,8 +348,10 @@ sub add { . $self->{authdir} . '/' . $modulefile; - copy( $modulepath, dirname( $target ) ) - or croak "Copy failed: $!"; + my $copy_status = mirror( $file_uri, $target ); + if (is_error($copy_status)) { + croak "Copy failed: ".status_message($copy_status); + } $self->_updperms( $target ); From a103a52e97ea6619ab50955658661deab3f86918 Mon Sep 17 00:00:00 2001 From: Tim Bunce Date: Thu, 11 Aug 2011 08:46:17 -0600 Subject: [PATCH 11/12] Remove limited file not found check, add test for file not found --- lib/CPAN/Mini/Inject.pm | 3 --- t/add.t | 7 +++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index c2a14b1..08b7047 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -335,9 +335,6 @@ sub add { croak "Can not write to repository: $repository" unless ( -w $repository ); - croak "Can not read module file: $options{file}" - unless -r $options{file}; - $self->readlist unless exists( $self->{modulelist} ); $self->{authdir} = $self->_authordir( $authorid, $repository ); diff --git a/t/add.t b/t/add.t index 09d6ada..ee752be 100644 --- a/t/add.t +++ b/t/add.t @@ -1,4 +1,4 @@ -use Test::More tests => 17; +use Test::More tests => 18; use Test::Exception; use CPAN::Mini::Inject; @@ -19,7 +19,7 @@ $mcpi->add( module => 'CPAN::Mini::Inject', authorid => 'SSORICHE', version => '0.02', - file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' + file => 'file:t/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' )->add( # Injecting multiple modules, with different versions module => {Foo => '1.0', Bar => '2.0'}, @@ -74,6 +74,9 @@ throws_ok {$mcpi->add( module => [], authorid => 'AUTHOR', file => 'My-Modules-1 throws_ok {$mcpi->add( module => {Foo => undef}, authorid => 'AUTHOR', file => 'My-Modules-1.0.tar.gz') } qr/Must specify 'version'/, 'No default version and no explicit version either'; +throws_ok {$mcpi->add( module => {Foo => 1}, authorid => 'AUTHOR', file => 'None-Such-0.0.tar.gz') } + qr/Copy failed: Not Found/, 'file not found'; + SKIP: { skip "Not a UNIX system", 2 if ( $^O =~ /^MSWin/ ); is( ( stat( 't/local/MYCPAN/authors/id/S/SS/SSORICHE' ) )[2] & 07777, From 5b6e80a56c21332bbbe53071776079742858dcd4 Mon Sep 17 00:00:00 2001 From: Tim Bunce Date: Thu, 11 Aug 2011 09:06:13 -0600 Subject: [PATCH 12/12] add some overview to the docs - would have helped me get my head around it faster --- bin/mcpani | 14 +++++++++++++- lib/CPAN/Mini/Inject.pm | 12 +++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/bin/mcpani b/bin/mcpani index 388a380..f3bf636 100644 --- a/bin/mcpani +++ b/bin/mcpani @@ -280,11 +280,23 @@ Options: -V, --version Version information. --signing-key See CPAN::Checksums $SIGNING_KEY +=head1 DESCRIPTION + +mcpani uses CPAN::Mini to build or update a I CPAN mirror from a +I one. It adds two extra features: + +1. an additional I of distribution files and related information +(author and module versions), separate from the local and remote mirrors, to +which you can add your own distribution files. + +2. the ability to I the distribution files from your I +into a I CPAN mirror. + =head1 COMMAND LINE OPTIONS =head2 --add -Add a module to the repository for later inclusion in the CPAN Mini +Add a module to the repository for later inclusion in a CPAN Mini mirror. The add command requires the following parameters: =over 4 diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index 08b7047..d1dc752 100644 --- a/lib/CPAN/Mini/Inject.pm +++ b/lib/CPAN/Mini/Inject.pm @@ -56,9 +56,15 @@ probably want to look at the mcpani command instead. =head1 DESCRIPTION -CPAN::Mini::Inject uses CPAN::Mini to build or update a local CPAN mirror -then adds modules from your repository to it, allowing the inclusion -of private modules in a minimal CPAN mirror. +CPAN::Mini::Inject uses CPAN::Mini to build or update a I CPAN mirror +from a I one. It adds two extra features: + +1. an additional I of distribution files and related information +(author and module versions), separate from the local and remote mirrors, to +which you can add your own distribution files. + +2. the ability to I the distribution files from your I +into a I CPAN mirror. =head1 METHODS