package PAR::Repository; use 5.006; use strict; use warnings; use Carp qw/croak/; use File::Spec::Functions qw/catfile catdir splitpath/; use File::Path qw/mkpath/; use PAR::Dist qw//; use YAML::Syck qw//; use File::Copy qw//; use Cwd qw//; use Archive::Zip qw//; use File::Temp qw//; use version qw//; use PAR::Indexer qw//; use PAR::Repository::Zip; use PAR::Repository::DBM; use PAR::Repository::Query; our @ISA = qw( PAR::Repository::Zip PAR::Repository::DBM PAR::Repository::Query ); use constant REPOSITORY_INFO_FILE => 'repository_info.yml'; our $VERSION = '0.20'; our $VERBOSE = 0; # does the running platform have symlinks? our $Supports_Symlinks = exists($ENV{PAR_REPOSITORY_SYMLINK_SUPPORT}) ? $ENV{PAR_REPOSITORY_SYMLINK_SUPPORT} : eval { symlink("",""); 1 }; # template for a repository_info.yml file our $Info_Template = { repository_version => $VERSION, # on platforms which don't have symlinks, fake them for new repositories! ($Supports_Symlinks ? () : (fake_symlinks => 1)), }; # Hash of compatible PAR::Repository versions our $Compatible_Versions = { $VERSION => 1, '0.19'=> 1, '0.18_01'=> 1, '0.17_01'=> 1, '0.17'=> 1, '0.16_02' => 1, '0.16_01' => 1, '0.16' => 1, '0.15' => 1, '0.14' => 1, '0.13' => 1, '0.12' => 1, '0.11' => 1, '0.10' => 1, '0.03' => 1, '0.02' => 1, }; =head1 NAME PAR::Repository - Create and modify PAR repositories =head1 SYNOPSIS # Usually, you want to use the 'parrepo' script which comes with # this distribution. use PAR::Repository; my $repo = PAR::Repository->new( path => '/path/to/repository' ); # creates a new repository if it doesn't exist, opens it if it # does exist. $repo->inject( file => 'Foo-Bar-0.01-x86_64-linux-gnu-thread-multi-5.8.7.par' ); $repo->remove( file => '...' ); $repo->query_module(regex => 'Foo::Bar'); =head1 DESCRIPTION This module is intended for creation and maintenance of PAR repositories. A PAR repository is collection of F<.par> archives which contain Perl code and associated libraries for use on specific platforms. In the most common case, these archives differ from CPAN distributions in that they ship the (possibly compiled) output of C in the F subdirectory of the CPAN distribution's build directory. You can access a PAR repository using the L module or the L module which provides syntactic sugar around the client. L allows you to load libraries from repositories on demand. =head2 PAR REPOSITORIES A PAR repository is, basically, just a directory with certain stuff in it. It contains: =over 2 =item modules_dists.dbm.zip An index that maps module names to file names. Details can be found in L. =item symlinks.dbm.zip An index that maps file names to other files. You shouldn't have to care about it. Details can be found in L. =item scripts_dists.dbm.zip An index that maps script names to file names. Details can be found in L. =item repository_info.yml A simple YAML file which contains meta information for the repository. It currently contains the following bits of information: =item dbm_checksums.txt A text file associating the DBM files with their MD5 checksums. (new in 0.15) =over 2 =item repository_version The version of PAR::Repository this repository was created with. When opening an existing repository, PAR::Repository checks that the repository was created by a compatible PAR::Repository version. Similarily, PAR::Repository::Client checks that the repository has a compatible version. =back =item I directories Your system architecture is identified with a certain string. For example, my development box is C. For every such architecture for which there are PAR archives in the repository, there is a directory with the name of the architecture. There is one special directory called C which is meant for PAR archives that are architecture independent. (Usually I modules.) In every such architecture directory, there is a number of directories for every Perl version. (5.6.0, 5.6.1, 5.8.0, ...) Again, there is a special directory for modules that work with any version of Perl. This directory is called C. Of course, a module won't run with Perl 4 and probably not even with 5.001. Whether a module works with I of perl is something you need to decide when injecting modules into the repository and depends on the scope of the repository. These inner directories contain the PAR archives. The directories exist mostly because large repositories with a lot of modules for a lot of architectures would otherwise have too large directory lists. =item PAR archives Within the I directories come the actual PAR archives. The name of each such file is of the following form: I-I-I-I.par =back =head1 METHODS Following is a list of class and instance methods. (Instance methods until otherwise mentioned.) Other methods callable on C objects are inherited from classes listed in the I section. =cut =head2 new Creates a new PAR::Repository object. Takes named arguments. Mandatory paramater: C should be the path to the PAR repository. If the repository does not exist yet, it is created empty. If the repository exists, it is I. That means any modifications you apply to the repository object are applied to the I repository on disk. Optional parameters: Additionally, you may supply the C 1> or C 1> parameters. Both default to false. I will convert an existing repository that uses symbolic links to using no symbolic links as if it had been created with the I option. If the repository has to be created, I flags it as using no symbolic links. Copies will be used instead. this may result is a larger but more portable repository. I implies I. See also I below. I is the default for creating new repositories on platforms which do not support symlinks. =cut sub new { my $proto = shift; my $class = ref($proto) || $proto; croak(__PACKAGE__."->new() takes an even number of arguments.") if @_ % 2; my %args = @_; croak(__PACKAGE__."->new() needs a 'path' argument.") if not defined $args{path}; my $path = $args{path}; my $self = bless { path => $path, # The tied dbm hashes modules_hash => undef, symlinks_hash => undef, scripts_hash => undef, dependencies_hash => undef, # The temp dbm files on disk modules_dbm_temp_file => undef, symlinks_dbm_temp_file => undef, scripts_dbm_temp_file => undef, dependencies_dbm_temp_file => undef, # The YAML info as Perl data structure info => undef, } => $class; $self->verbose(2, "Created new repository object in path '$path'"); # check that the repository exists or create it. my $mod_dbm = catfile($path, PAR::Repository::DBM::MODULES_DBM_FILE()); my $sym_dbm = catfile($path, PAR::Repository::DBM::SYMLINKS_DBM_FILE()); my $scr_dbm = catfile($path, PAR::Repository::DBM::SCRIPTS_DBM_FILE()); my $dep_dbm = catfile($path, PAR::Repository::DBM::DEPENDENCIES_DBM_FILE()); my $info_file = catfile($path, PAR::Repository::REPOSITORY_INFO_FILE()); if (-d $path and -f $mod_dbm.'.zip' and -f $sym_dbm.'.zip' and -f $info_file ) { # everything is in place. good. $self->verbose(3, "Repository exists"); # load repository info $self->{info} = YAML::Syck::LoadFile($info_file); if ( not defined $self->{info} or not exists $self->{info}{repository_version} ) { croak("Repository exists, but it does not contain a valid repository_info.yml file."); } elsif ( not exists $Compatible_Versions->{$self->{info}{repository_version}} ) { croak("Repository exists, but it was created with an incompatible version of PAR::Repository (".$self->{info}{repository_version}.")"); } # the following is a special case because the "incompatible changes # with every "\d+.\d" release" rule was introduced in 0.10 elsif ( $Compatible_Versions->{$self->{info}{repository_version}} eq '0.03' ) { $self->_update_info_version or return (); $self->verbose(3, "Updated repository version"); } if ($args{convert_symlinks}) { $self->_convert_symlinks(); } if (!$Supports_Symlinks and !$self->{info}{fake_symlinks}) { croak("Repository may use symlinks but your platform does not support them. " ."Use the convert_symlinks => 1 option to the PAR::Repository constructor " ."to convert the repository to one which does not use symbolic links."); } $self->verbose(3, "Opened repository successfully"); # Generate scripts db and upgrade repository version # if the scripts db doesn't exist. if (not -f $scr_dbm.'.zip') { $self->verbose(1, "Upgrading repository version to $VERSION"); $self->_update_info_version or return (); $self->verbose(3, "Creating scripts database"); $self->_create_dbm($scr_dbm.'.zip'); } # Generate deps db and upgrade repository version # if the deps db doesn't exist. if (not -f $dep_dbm.'.zip') { $self->verbose(1, "Upgrading repository version to $VERSION"); $self->_update_info_version or return (); $self->verbose(3, "Creating dependencies database"); $self->_create_dbm($dep_dbm.'.zip'); } } # end if everything is in place else { $self->verbose(3, "Repository doesn't exist yet"); $self->_create_repository($path, !$Supports_Symlinks||$args{fake_symlinks}); } return $self; } # creates a new repository # called by the constructor if the directory doesn't exist sub _create_repository { my $self = shift; my $path = shift; my $fake_symlinks = shift; if (-d $path) { croak("The repository path exists, but is not a repository. Delete it to create a new repository."); } mkpath([$path]); my $mod_dbm = catfile($path, PAR::Repository::DBM::MODULES_DBM_FILE()); my $sym_dbm = catfile($path, PAR::Repository::DBM::SYMLINKS_DBM_FILE()); my $scr_dbm = catfile($path, PAR::Repository::DBM::SCRIPTS_DBM_FILE()); my $dep_dbm = catfile($path, PAR::Repository::DBM::DEPENDENCIES_DBM_FILE()); my $info_file = catfile($path, PAR::Repository::REPOSITORY_INFO_FILE()); $self->verbose(3, "Creating repository databases"); foreach my $dbm ($mod_dbm, $sym_dbm, $scr_dbm, $dep_dbm) { $self->_create_dbm($dbm.'.zip'); } my $info_copy = {%$Info_Template}; $info_copy->{fake_symlinks} = 1 if $fake_symlinks; YAML::Syck::DumpFile($info_file, $info_copy); $self->{info} = YAML::Syck::LoadFile($info_file); } # converts all symlinks to files, sets {info}->{fake_symlinks}, # and saves it # called by the constructor sub _convert_symlinks { my $self = shift; $self->{error} = undef; $self->verbose(1, "Converting symlinks to files!"); # change to repo path my $old_dir = Cwd::cwd(); chdir($self->{path}); my $info_file = catfile($self->{path}, PAR::Repository::REPOSITORY_INFO_FILE()); my ($symdbm, $temp_file) = $self->symlinks_dbm; while (my ($file, $symlinks) = each %$symdbm) { my ($distname, $distver, $arch, $perlver) = PAR::Dist::parse_dist_name($file); my $file_fullpath = File::Spec->catfile($arch, $perlver, $file); foreach my $symlink_file (@$symlinks) { my ($distname, $distver, $arch, $perlver) = PAR::Dist::parse_dist_name($symlink_file); my $symlink_file_fullpath = File::Spec->catfile($arch, $perlver, $symlink_file); # first unlink or else File::Copy may claim it can't copy because the files are # the same. (unlink( $symlink_file_fullpath ) and File::Copy::copy( $file_fullpath, $symlink_file_fullpath )) or chdir($old_dir), die "Error converting symlinks in repository to real files: Could not copy " ."'$file' to '$symlink_file'. Your repository may be in an inconsistent " ."state now. Reason: $!"; } } chdir($old_dir); $self->{info}{fake_symlinks} = 1; YAML::Syck::DumpFile($info_file, $self->{info}); $self->{info} = YAML::Syck::LoadFile($info_file); return 1; } =head2 inject Injects a new PAR distribution into the repository. Takes named parameters. Mandatory parameters: I, the path and filename of the PAR distribution to inject. The name of the file can be used to automatically determine the I, I, I, and I parameters if the form of the file name is as follows: Dist-Name-0.01-x86_64-linux-gnu-thread-multi-5.8.7.par This would set C 'Dist-Name', distversion => '0.01', arch => 'linux-gnu-thread-multi', perlversion => '5.8.7'>. You can override this automatic detection using the corresponding parameters. If the file exists in the repository, inject returns false. If the file was added successfully, inject returns true. See the C parameter for details. C scans the distribution for modules and indexes these in the modules-dists dbm. Additionally, it scans the distribution for scripts in the C