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 File::Fetch; use Archive::Extract; use IPC::Cmd qw[can_run]; use File::Temp qw[tempdir]; use File::Basename qw[dirname]; 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::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. Functions exist to check if source files are still C 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->__update_custom_module_sources $cb->__update_custom_module_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->__create_custom_module_entries $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; } } ### if we're explicitly asked to update the sources, or if the ### standard source files are out of date, update the custom sources ### as well $self->__update_custom_module_sources( verbose => $verbose ) if $update_source or !$uptodate; 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 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 $verbose; my $tmpl = { name => { required => 1 }, path => { default => $conf->get_conf('base') }, verbose => { default => $conf->get_conf('verbose'), store => \$verbose }, }; my $args = check( $tmpl, \%hash ) or return; my $path = $args->{path}; { ### 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), $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; } $self->_update_timestamp( file => File::Spec->catfile($path, $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}; ### update them if the other sources are also deemed out of date unless( $uptodate ) { $self->__update_custom_module_sources( verbose => $args->{verbose} ) or error(loc("Could not update custom module sources")); } ### add custom sources here $self->__create_custom_module_entries( verbose => $args->{verbose} ) or error(loc("Could not create custom module entries")); ### 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 Id 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 Id format if C 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; } =head2 $file = $cb->_add_custom_module_source( uri => URI, [verbose => BOOL] ); Adds a custom source index and updates it based on the provided URI. Returns the full path to the index file on success or false on failure. =cut sub _add_custom_module_source { my $self = shift; my $conf = $self->configure_object; my %hash = @_; my($verbose,$uri); my $tmpl = { verbose => { default => $conf->get_conf('verbose'), store => \$verbose }, uri => { required => 1, store => \$uri } }; check( $tmpl, \%hash ) or return; ### what index file should we use on disk? my $index = $self->__custom_module_source_index_file( uri => $uri ); ### already have it. if( IS_FILE->( $index ) ) { msg(loc("Source '%1' already added", $uri)); return 1; } ### do we need to create the targe dir? { my $dir = dirname( $index ); unless( IS_DIR->( $dir ) ) { $self->_mkdir( dir => $dir ) or return } } ### write the file my $fh = OPEN_FILE->( $index => '>' ) or do { error(loc("Could not open index file for '%1'", $uri)); return; }; ### basically we 'touched' it. Check the return value, may be ### important on win32 and similar OS, where there's file length ### limits close $fh or do { error(loc("Could not write index file to disk for '%1'", $uri)); return; }; $self->__update_custom_module_source( remote => $uri, local => $index, verbose => $verbose, ) or do { ### we faild to update it, we probably have an empty ### possibly silly filename on disk now -- remove it 1 while unlink $index; return; }; return $index; } =head2 $index = $cb->__custom_module_source_index_file( uri => $uri ); Returns the full path to the encoded index file for C<$uri>, as used by all C routines. =cut sub __custom_module_source_index_file { my $self = shift; my $conf = $self->configure_object; my %hash = @_; my($verbose,$uri); my $tmpl = { uri => { required => 1, store => \$uri } }; check( $tmpl, \%hash ) or return; my $index = File::Spec->catfile( $conf->get_conf('base'), $conf->_get_build('custom_sources'), $self->_uri_encode( uri => $uri ), ); return $index; } =head2 $file = $cb->_remove_custom_module_source( uri => URI, [verbose => BOOL] ); Removes a custom index file based on the URI provided. Returns the full path to the index file on success or false on failure. =cut sub _remove_custom_module_source { my $self = shift; my $conf = $self->configure_object; my %hash = @_; my($verbose,$uri); my $tmpl = { verbose => { default => $conf->get_conf('verbose'), store => \$verbose }, uri => { required => 1, store => \$uri } }; check( $tmpl, \%hash ) or return; ### use uri => local, instead of the other way around my %files = reverse $self->__list_custom_module_sources; ### On VMS the case of key to %files can be either exact or lower case ### XXX abstract this lookup out? --kane my $file = $files{ $uri }; $file = $files{ lc $uri } if !defined($file) && ON_VMS; unless (defined $file) { error(loc("No such custom source '%1'", $uri)); return; }; 1 while unlink $file; if( IS_FILE->( $file ) ) { error(loc("Could not remove index file '%1' for custom source '%2'", $file, $uri)); return; } msg(loc("Successfully removed index file for '%1'", $uri), $verbose); return $file; } =head2 %files = $cb->__list_custom_module_sources This method scans the 'custom-sources' directory in your base directory for additional sources to include in your module tree. Returns a list of key value pairs as follows: /full/path/to/source/file%3Fencoded => http://decoded/mirror/path =cut sub __list_custom_module_sources { my $self = shift; my $conf = $self->configure_object; my $dir = File::Spec->catdir( $conf->get_conf('base'), $conf->_get_build('custom_sources'), ); unless( IS_DIR->( $dir ) ) { msg(loc("No '%1' dir, skipping custom sources", $dir)); return; } ### unencode the files ### skip ones starting with # though my %files = map { my $org = $_; my $dec = $self->_uri_decode( uri => $_ ); File::Spec->catfile( $dir, $org ) => $dec } grep { $_ !~ /^#/ } READ_DIR->( $dir ); return %files; } =head2 $bool = $cb->__update_custom_module_sources( [verbose => BOOL] ); Attempts to update all the index files to your custom module sources. If the index is missing, and it's a C uri, it will generate a new local index for you. Return true on success, false on failure. =cut sub __update_custom_module_sources { my $self = shift; my $conf = $self->configure_object; my %hash = @_; my $verbose; my $tmpl = { verbose => { default => $conf->get_conf('verbose'), store => \$verbose } }; check( $tmpl, \%hash ) or return; my %files = $self->__list_custom_module_sources; ### uptodate check has been done a few levels up. my $fail; while( my($local,$remote) = each %files ) { $self->__update_custom_module_source( remote => $remote, local => $local, verbose => $verbose, ) or ( $fail++, next ); } error(loc("Failed updating one or more remote sources files")) if $fail; return if $fail; return 1; } =head2 $ok = $cb->__update_custom_module_source Attempts to update all the index files to your custom module sources. If the index is missing, and it's a C uri, it will generate a new local index for you. Return true on success, false on failure. =cut sub __update_custom_module_source { my $self = shift; my $conf = $self->configure_object; my %hash = @_; my($verbose,$local,$remote); my $tmpl = { verbose => { default => $conf->get_conf('verbose'), store => \$verbose }, local => { store => \$local, allow => FILE_EXISTS }, remote => { required => 1, store => \$remote }, }; check( $tmpl, \%hash ) or return; msg( loc("Updating sources from '%1'", $remote), $verbose); ### if you didn't provide a local file, we'll look in your custom ### dir to find the local encoded version for you $local ||= do { ### find all files we know of my %files = reverse $self->__list_custom_module_sources or do { error(loc("No custom modules sources defined -- need '%1' argument", 'local')); return; }; ### On VMS the case of key to %files can be either exact or lower case ### XXX abstract this lookup out? --kane my $file = $files{ $remote }; $file = $files{ lc $remote } if !defined ($file) && ON_VMS; ### return the local file we're supposed to use $file or do { error(loc("Remote source '%1' unknown -- needs '%2' argument", $remote, 'local')); return; }; }; my $uri = join '/', $remote, $conf->_get_source('custom_index'); my $ff = File::Fetch->new( uri => $uri ); ### tempdir doesn't clean up by default, as opposed to tempfile() ### so add it explicitly. my $dir = tempdir( CLEANUP => 1 ); my $res = do { local $File::Fetch::WARN = 0; local $File::Fetch::WARN = 0; $ff->fetch( to => $dir ); }; ### couldn't get the file unless( $res ) { ### it's not a local scheme, so can't auto index unless( $ff->scheme eq 'file' ) { error(loc("Could not update sources from '%1': %2", $remote, $ff->error )); return; ### it's a local uri, we can index it ourselves } else { msg(loc("No index file found at '%1', generating one", $ff->uri), $verbose ); ### ON VMS, if you are working with a UNIX file specification, ### you need currently use the UNIX variants of the File::Spec. my $ff_path = do { my $file_class = 'File::Spec'; $file_class .= '::Unix' if ON_VMS; $file_class->catdir( File::Spec::Unix->splitdir( $ff->path ) ); }; $self->__write_custom_module_index( path => $ff_path, to => $local, verbose => $verbose, ) or return; ### XXX don't write that here, __write_custom_module_index ### already prints this out #msg(loc("Index file written to '%1'", $to), $verbose); } ### copy it to the real spot and update it's timestamp } else { $self->_move( file => $res, to => $local ) or return; $self->_update_timestamp( file => $local ); msg(loc("Index file saved to '%1'", $local), $verbose); } return $local; } =head2 $bool = $cb->__write_custom_module_index( path => /path/to/packages, [to => /path/to/index/file, verbose => BOOL] ) Scans the C you provided for packages and writes an index with all the available packages to C<$path/packages.txt>. If you'd like the index to be written to a different file, provide the C argument. Returns true on success and false on failure. =cut sub __write_custom_module_index { my $self = shift; my $conf = $self->configure_object; my %hash = @_; my ($verbose, $path, $to); my $tmpl = { verbose => { default => $conf->get_conf('verbose'), store => \$verbose }, path => { required => 1, allow => DIR_EXISTS, store => \$path }, to => { store => \$to }, }; check( $tmpl, \%hash ) or return; ### no explicit to? then we'll use our default $to ||= File::Spec->catfile( $path, $conf->_get_source('custom_index') ); my @files; require File::Find; File::Find::find( sub { ### let's see if A::E can even parse it my $ae = do { local $Archive::Extract::WARN = 0; local $Archive::Extract::WARN = 0; Archive::Extract->new( archive => $File::Find::name ) } or return; ### it's a type A::E recognize, so we can add it $ae->type or return; ### neither $_ nor $File::Find::name have the chunk of the path in ### it starting $path -- it's either only the filename, or the full ### path, so we have to strip it ourselves ### make sure to remove the leading slash as well. my $copy = $File::Find::name; my $re = quotemeta($path); $copy =~ s|^$re[\\/]?||i; push @files, $copy; }, $path ); ### does the dir exist? if not, create it. { my $dir = dirname( $to ); unless( IS_DIR->( $dir ) ) { $self->_mkdir( dir => $dir ) or return } } ### create the index file my $fh = OPEN_FILE->( $to => '>' ) or return; print $fh "$_\n" for @files; close $fh; msg(loc("Successfully written index file to '%1'", $to), $verbose); return $to; } =head2 $bool = $cb->__create_custom_module_entries( [verbose => BOOL] ) Creates entries in the module tree based upon the files as returned by C<__list_custom_module_sources>. Returns true on success, false on failure. =cut ### use $auth_obj as a persistant version, so we don't have to recreate ### modules all the time { my $auth_obj; sub __create_custom_module_entries { my $self = shift; my $conf = $self->configure_object; my %hash = @_; my $verbose; my $tmpl = { verbose => { default => $conf->get_conf('verbose'), store => \$verbose }, }; check( $tmpl, \%hash ) or return undef; my %files = $self->__list_custom_module_sources; while( my($file,$name) = each %files ) { msg(loc("Adding packages from custom source '%1'", $name), $verbose); my $fh = OPEN_FILE->( $file ) or next; while( <$fh> ) { chomp; next if /^#/; next unless /\S+/; ### join on / -- it's a URI after all! my $parse = join '/', $name, $_; ### try to make a module object out of it my $mod = $self->parse_module( module => $parse ) or ( error(loc("Could not parse '%1'", $_)), next ); ### mark this object with a custom author $auth_obj ||= do { my $id = CUSTOM_AUTHOR_ID; ### if the object is being created for the first time, ### make sure there's an entry in the author tree as ### well, so we can search on the CPAN ID $self->author_tree->{ $id } = CPANPLUS::Module::Author::Fake->new( cpanid => $id ); }; $mod->author( $auth_obj ); ### and now add it to the modlue tree -- this MAY ### override things of course if( my $old_mod = $self->module_tree( $mod->module ) ) { ### On VMS use the old module name to get the real case $mod->module( $old_mod->module ) if ON_VMS; msg(loc("About to overwrite module tree entry for '%1' with '%2'", $mod->module, $mod->package), $verbose); } ### mark where it came from $mod->description( loc("Custom source from '%1'",$name) ); ### store it in the module tree $self->module_tree->{ $mod->module } = $mod; } } return 1; } } # Local variables: # c-indentation-style: bsd # c-basic-offset: 4 # indent-tabs-mode: nil # End: # vim: expandtab shiftwidth=4: 1;