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.* diff --git a/bin/mcpani b/bin/mcpani index 7d80328..f3bf636 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,32 @@ 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} if $options{module}; $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, + version => $options{version}, + 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 +223,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,64 +255,90 @@ 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 + + --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 + --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 + +=head1 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 +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 -=item --module +=item --module NAME=VERSION + +=item --module NAME + +This is the NAME and VERSION of the module to add. This option can be +repeated for multiple modules. If you do not specify a VERSION here, +it will be taken from the C<--modversion> option. -This is the name of the module (ie CPAN::Mini::Inject). +=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). -The options --module and --modversion are still recognized. If the -same module/version is found in the META.yml it is not duplicated. +=item --discover-packages -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. +This option will attempt to discover the modules in your tar.gz +by examining all the F<.pm> files in the F directory. + +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 +348,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 diff --git a/lib/CPAN/Mini/Inject.pm b/lib/CPAN/Mini/Inject.pm index 738e027..d1dc752 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 @@ -41,7 +42,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; @@ -49,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 @@ -248,28 +261,59 @@ 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 * authorid +=item * version -CPAN author id. This does not have to be a real author id. +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 * version +=item * authorid -The modules version number. +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 -=head3 Example +=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 with different versions: + + add( module => { + Animal => '0.1', + Animal::Bear => '1.2', + Animal::Zebra => '2.6', + }, + authorid => 'AUTHOR', + 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 @@ -277,48 +321,61 @@ sub add { my $self = shift; my %options = @_; - my $optionchk - = _optionchk( \%options, qw/module authorid version file/ ); + _optionchk( %options ); # Croaks if invalid! + + 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} ? + $options{version} : 'undef'; + + my $modules = ref $options{module} eq 'HASH' ? + $options{module} : { $options{module} => $defaultversion }; + + 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 read module file: $options{file}" - unless -r $options{file}; + croak "Can not write to repository: $repository" + unless ( -w $repository ); - 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 ) ) - 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 ); - # remove old version from the list - @{ $self->{modulelist} } - = grep { $_ !~ m/\A$options{module}\s+/ } @{ $self->{modulelist} }; + foreach my $modulename (sort keys %$modules) { - push( - @{ $self->{modulelist} }, - _fmtmodule( - $options{module}, File::Spec::Unix->catfile( File::Spec->splitdir( $self->{authdir} ), $modulefile ), - $options{version} - ) - ); + # 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} } + = grep { $_ !~ m/\A$modulename\s+/ } @{ $self->{modulelist} }; + + push( + @{ $self->{modulelist} }, + _fmtmodule($modulename, + File::Spec::Unix->catfile(File::Spec->splitdir( $self->{authdir} ), $modulefile ), + $moduleversion ) + ); + } return $self; } @@ -505,15 +562,33 @@ sub _updperms { } sub _optionchk { - my ( $options, @list ) = @_; - my @missing; + my ( %options ) = @_; + + # TODO: Don't call them "options" if they are required! + + my @missing_options = grep { not $options{$_} } qw(authorid file module); + croak "Required option not specified: " . join ' ', @missing_options + if @missing_options; + + my ($mod, $ver) = @options{qw(module version)}; + + + + 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 "Must specify 'version' if 'module' does not contain a version for each module" + if ( not $ver and grep { not $mod->{$_} } keys %$mod ); + } + else { - for my $option ( @list ) { - push @missing, $option - unless defined $$options{$option}; + croak "The 'module' argument must be a string or hashref"; } - return join ' ', @missing; } sub _make_path { diff --git a/t/add.t b/t/add.t index 1e52bfb..ee752be 100644 --- a/t/add.t +++ b/t/add.t @@ -1,4 +1,5 @@ -use Test::More tests => 6; +use Test::More tests => 18; +use Test::Exception; use CPAN::Mini::Inject; use File::Path; @@ -18,8 +19,19 @@ $mcpi->add( module => 'CPAN::Mini::Inject', authorid => 'SSORICHE', version => '0.02', + 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'}, + authorid => 'SSORICHE', + file => 't/local/mymodules/CPAN-Mini-Inject-0.01.tar.gz' +)->add( + # Injecting multiple modules, with different versions, and a default version + 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' - ); +); my $soriche_path = File::Spec->catfile( 'S', 'SS', 'SSORICHE' ); is( $mcpi->{authdir}, $soriche_path, 'author directory' ); @@ -27,13 +39,44 @@ 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', + '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} } ), "Added: $module" ); +} + 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', module => {}) } + qr/Required option not specified: file/, 'Missing file argument'; + +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/Required option not specified: module/, 'Missing module argument'; + +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( 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( 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,