#!/usr/bin/env perl # This chunk of stuff was generated by App::FatPacker. To find the original # file's code, look for the end of this BEGIN block or the string 'FATPACK' BEGIN { my %fatpacked; $fatpacked{"CPAN/Common/Index.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_COMMON_INDEX'; use 5.008001; use strict; use warnings; package CPAN::Common::Index; # ABSTRACT: Common library for searching CPAN modules, authors and distributions our $VERSION = '0.010'; use Carp (); use Class::Tiny; #--------------------------------------------------------------------------# # Document abstract methods #--------------------------------------------------------------------------# #pod =method search_packages (ABSTRACT) #pod #pod $result = $index->search_packages( { package => "Moose" }); #pod @result = $index->search_packages( \%advanced_query ); #pod #pod Searches the index for a package such as listed in the CPAN #pod F<02packages.details.txt> file. The query must be provided as a hash #pod reference. Valid keys are #pod #pod =for :list #pod * package -- a string, regular expression or code reference #pod * version -- a version number or code reference #pod * dist -- a string, regular expression or code reference #pod #pod If the query term is a string or version number, the query will be for an exact #pod match. If a code reference, the code will be called with the value of the #pod field for each potential match. It should return true if it matches. #pod #pod Not all backends will implement support for all fields or all types of queries. #pod If it does not implement either, it should "decline" the query with an empty #pod return. #pod #pod The return should be context aware, returning either a #pod single result or a list of results. #pod #pod The result must be formed as follows: #pod #pod { #pod package => 'MOOSE', #pod version => '2.0802', #pod uri => "cpan:///distfile/ETHER/Moose-2.0802.tar.gz" #pod } #pod #pod The C field should be a valid URI. It may be a L or any other #pod URI. (It is up to a client to do something useful with any given URI scheme.) #pod #pod =method search_authors (ABSTRACT) #pod #pod $result = $index->search_authors( { id => "DAGOLDEN" }); #pod @result = $index->search_authors( \%advanced_query ); #pod #pod Searches the index for author data such as from the CPAN F<01mailrc.txt> file. #pod The query must be provided as a hash reference. Valid keys are #pod #pod =for :list #pod * id -- a string, regular expression or code reference #pod * fullname -- a string, regular expression or code reference #pod * email -- a string, regular expression or code reference #pod #pod If the query term is a string, the query will be for an exact match. If a code #pod reference, the code will be called with the value of the field for each #pod potential match. It should return true if it matches. #pod #pod Not all backends will implement support for all fields or all types of queries. #pod If it does not implement either, it should "decline" the query with an empty #pod return. #pod #pod The return should be context aware, returning either a single result or a list #pod of results. #pod #pod The result must be formed as follows: #pod #pod { #pod id => 'DAGOLDEN', #pod fullname => 'David Golden', #pod email => 'dagolden@cpan.org', #pod } #pod #pod The C field may not reflect an actual email address. The 01mailrc file #pod on CPAN often shows "CENSORED" when email addresses are concealed. #pod #pod =cut #--------------------------------------------------------------------------# # stub methods #--------------------------------------------------------------------------# #pod =method index_age #pod #pod $epoch = $index->index_age; #pod #pod Returns the modification time of the index in epoch seconds. This may not make sense #pod for some backends. By default it returns the current time. #pod #pod =cut sub index_age { time } #pod =method refresh_index #pod #pod $index->refresh_index; #pod #pod This ensures the index source is up to date. For example, a remote #pod mirror file would be re-downloaded. By default, it does nothing. #pod #pod =cut sub refresh_index { 1 } #pod =method attributes #pod #pod Return attributes and default values as a hash reference. By default #pod returns an empty hash reference. #pod #pod =cut sub attributes { {} } #pod =method validate_attributes #pod #pod $self->validate_attributes; #pod #pod This is called by the constructor to validate any arguments. Subclasses #pod should override the default one to perform validation. It should not be #pod called by application code. By default, it does nothing. #pod #pod =cut sub validate_attributes { 1 } 1; # vim: ts=4 sts=4 sw=4 et: __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Common::Index - Common library for searching CPAN modules, authors and distributions =head1 VERSION version 0.010 =head1 SYNOPSIS use CPAN::Common::Index::Mux::Ordered; use Data::Dumper; $index = CPAN::Common::Index::Mux::Ordered->assemble( MetaDB => {}, Mirror => { mirror => "http://cpan.cpantesters.org" }, ); $result = $index->search_packages( { package => "Moose" } ); print Dumper($result); # { # package => 'MOOSE', # version => '2.0802', # uri => "cpan:///distfile/ETHER/Moose-2.0802.tar.gz" # } =head1 DESCRIPTION This module provides a common library for working with a variety of CPAN index services. It is intentionally minimalist, trying to use as few non-core modules as possible. The C module is an abstract base class that defines a common API. Individual backends deliver the API for a particular index. As shown in the SYNOPSIS, one interesting application is multiplexing -- using different index backends, querying each in turn, and returning the first result. =head1 METHODS =head2 search_packages (ABSTRACT) $result = $index->search_packages( { package => "Moose" }); @result = $index->search_packages( \%advanced_query ); Searches the index for a package such as listed in the CPAN F<02packages.details.txt> file. The query must be provided as a hash reference. Valid keys are =over 4 =item * package -- a string, regular expression or code reference =item * version -- a version number or code reference =item * dist -- a string, regular expression or code reference =back If the query term is a string or version number, the query will be for an exact match. If a code reference, the code will be called with the value of the field for each potential match. It should return true if it matches. Not all backends will implement support for all fields or all types of queries. If it does not implement either, it should "decline" the query with an empty return. The return should be context aware, returning either a single result or a list of results. The result must be formed as follows: { package => 'MOOSE', version => '2.0802', uri => "cpan:///distfile/ETHER/Moose-2.0802.tar.gz" } The C field should be a valid URI. It may be a L or any other URI. (It is up to a client to do something useful with any given URI scheme.) =head2 search_authors (ABSTRACT) $result = $index->search_authors( { id => "DAGOLDEN" }); @result = $index->search_authors( \%advanced_query ); Searches the index for author data such as from the CPAN F<01mailrc.txt> file. The query must be provided as a hash reference. Valid keys are =over 4 =item * id -- a string, regular expression or code reference =item * fullname -- a string, regular expression or code reference =item * email -- a string, regular expression or code reference =back If the query term is a string, the query will be for an exact match. If a code reference, the code will be called with the value of the field for each potential match. It should return true if it matches. Not all backends will implement support for all fields or all types of queries. If it does not implement either, it should "decline" the query with an empty return. The return should be context aware, returning either a single result or a list of results. The result must be formed as follows: { id => 'DAGOLDEN', fullname => 'David Golden', email => 'dagolden@cpan.org', } The C field may not reflect an actual email address. The 01mailrc file on CPAN often shows "CENSORED" when email addresses are concealed. =head2 index_age $epoch = $index->index_age; Returns the modification time of the index in epoch seconds. This may not make sense for some backends. By default it returns the current time. =head2 refresh_index $index->refresh_index; This ensures the index source is up to date. For example, a remote mirror file would be re-downloaded. By default, it does nothing. =head2 attributes Return attributes and default values as a hash reference. By default returns an empty hash reference. =head2 validate_attributes $self->validate_attributes; This is called by the constructor to validate any arguments. Subclasses should override the default one to perform validation. It should not be called by application code. By default, it does nothing. =for Pod::Coverage method_names_here =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/Perl-Toolchain-Gang/CPAN-Common-Index.git =head1 AUTHOR David Golden =head1 CONTRIBUTORS =for stopwords David Golden Helmut Wollmersdorfer Kenichi Ishigaki Shoichi Kaji Tatsuhiko Miyagawa =over 4 =item * David Golden =item * Helmut Wollmersdorfer =item * Kenichi Ishigaki =item * Shoichi Kaji =item * Tatsuhiko Miyagawa =back =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2013 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut CPAN_COMMON_INDEX $fatpacked{"CPAN/Common/Index/LocalPackage.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_COMMON_INDEX_LOCALPACKAGE'; use 5.008001; use strict; use warnings; package CPAN::Common::Index::LocalPackage; # ABSTRACT: Search index via custom local CPAN package flatfile our $VERSION = '0.010'; use parent 'CPAN::Common::Index::Mirror'; use Class::Tiny qw/source/; use Carp; use File::Basename (); use File::Copy (); use File::Spec; use File::stat (); #pod =attr source (REQUIRED) #pod #pod Path to a local file in the form of 02packages.details.txt. It may #pod be compressed with a ".gz" suffix or it may be uncompressed. #pod #pod =attr cache #pod #pod Path to a local directory to store a (possibly uncompressed) copy #pod of the source index. Defaults to a temporary directory if not #pod specified. #pod #pod =cut sub BUILD { my $self = shift; my $file = $self->source; if ( !defined $file ) { Carp::croak("'source' parameter must be provided"); } elsif ( !-f $file ) { Carp::croak("index file '$file' does not exist"); } return; } sub cached_package { my ($self) = @_; my $package = File::Spec->catfile( $self->cache, File::Basename::basename($self->source) ); $package =~ s/\.gz$//; $self->refresh_index unless -r $package; return $package; } sub refresh_index { my ($self) = @_; my $source = $self->source; my $basename = File::Basename::basename($source); if ( $source =~ /\.gz$/ ) { Carp::croak "can't load gz source files without IO::Uncompress::Gunzip\n" unless $CPAN::Common::Index::Mirror::HAS_IO_UNCOMPRESS_GUNZIP; ( my $uncompressed = $basename ) =~ s/\.gz$//; $uncompressed = File::Spec->catfile( $self->cache, $uncompressed ); if ( !-f $uncompressed or File::stat::stat($source)->mtime > File::stat::stat($uncompressed)->mtime ) { no warnings 'once'; IO::Uncompress::Gunzip::gunzip( map { "$_" } $source, $uncompressed ) or Carp::croak "gunzip failed: $IO::Uncompress::Gunzip::GunzipError\n"; } } else { my $dest = File::Spec->catfile( $self->cache, $basename ); File::Copy::copy($source, $dest) if !-e $dest || File::stat::stat($source)->mtime > File::stat::stat($dest)->mtime; } return 1; } sub search_authors { return }; # this package handles packages only 1; # vim: ts=4 sts=4 sw=4 et: __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Common::Index::LocalPackage - Search index via custom local CPAN package flatfile =head1 VERSION version 0.010 =head1 SYNOPSIS use CPAN::Common::Index::LocalPackage; $index = CPAN::Common::Index::LocalPackage->new( { source => "mypackages.details.txt" } ); =head1 DESCRIPTION This module implements a CPAN::Common::Index that searches for packages in a local index file in the same form as the CPAN 02packages.details.txt file. There is no support for searching on authors. =head1 ATTRIBUTES =head2 source (REQUIRED) Path to a local file in the form of 02packages.details.txt. It may be compressed with a ".gz" suffix or it may be uncompressed. =head2 cache Path to a local directory to store a (possibly uncompressed) copy of the source index. Defaults to a temporary directory if not specified. =for Pod::Coverage attributes validate_attributes search_packages search_authors cached_package BUILD =head1 AUTHOR David Golden =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2013 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut CPAN_COMMON_INDEX_LOCALPACKAGE $fatpacked{"CPAN/Common/Index/MetaDB.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_COMMON_INDEX_METADB'; use 5.008001; use strict; use warnings; package CPAN::Common::Index::MetaDB; # ABSTRACT: Search index via CPAN MetaDB our $VERSION = '0.010'; use parent 'CPAN::Common::Index'; use Class::Tiny qw/uri/; use Carp; use CPAN::Meta::YAML; use HTTP::Tiny; #pod =attr uri #pod #pod A URI for the endpoint of a CPAN MetaDB server. The #pod default is L. #pod #pod =cut sub BUILD { my $self = shift; my $uri = $self->uri; $uri = "http://cpanmetadb.plackperl.org/v1.0/" unless defined $uri; # ensure URI ends in '/' $uri =~ s{/?$}{/}; $self->uri($uri); return; } sub search_packages { my ( $self, $args ) = @_; Carp::croak("Argument to search_packages must be hash reference") unless ref $args eq 'HASH'; # only support direct package query return unless keys %$args == 1 && exists $args->{package} && ref $args->{package} eq ''; my $mod = $args->{package}; my $res = HTTP::Tiny->new->get( $self->uri . "package/$mod" ); return unless $res->{success}; if ( my $yaml = CPAN::Meta::YAML->read_string( $res->{content} ) ) { my $meta = $yaml->[0]; if ( $meta && $meta->{distfile} ) { my $file = $meta->{distfile}; $file =~ s{^./../}{}; # strip leading return { package => $mod, version => $meta->{version}, uri => "cpan:///distfile/$file", }; } } return; } sub index_age { return time }; # pretend always current sub search_authors { return }; # not supported 1; # vim: ts=4 sts=4 sw=4 et: __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Common::Index::MetaDB - Search index via CPAN MetaDB =head1 VERSION version 0.010 =head1 SYNOPSIS use CPAN::Common::Index::MetaDB; $index = CPAN::Common::Index::MetaDB->new; =head1 DESCRIPTION This module implements a CPAN::Common::Index that searches for packages against the same CPAN MetaDB API used by L. There is no support for advanced package queries or searching authors. It just takes a package name and returns the corresponding version and distribution. =head1 ATTRIBUTES =head2 uri A URI for the endpoint of a CPAN MetaDB server. The default is L. =for Pod::Coverage attributes validate_attributes search_packages search_authors BUILD =head1 AUTHOR David Golden =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2013 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut CPAN_COMMON_INDEX_METADB $fatpacked{"CPAN/Common/Index/Mirror.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_COMMON_INDEX_MIRROR'; use 5.008001; use strict; use warnings; package CPAN::Common::Index::Mirror; # ABSTRACT: Search index via CPAN mirror flatfiles our $VERSION = '0.010'; use parent 'CPAN::Common::Index'; use Class::Tiny qw/cache mirror/; use Carp; use CPAN::DistnameInfo; use File::Basename (); use File::Fetch; use File::Temp 0.19; # newdir use Search::Dict 1.07; use Tie::Handle::SkipHeader; use URI; our $HAS_IO_UNCOMPRESS_GUNZIP = eval { require IO::Uncompress::Gunzip }; #pod =attr mirror #pod #pod URI to a CPAN mirror. Defaults to C. #pod #pod =attr cache #pod #pod Path to a local directory to store copies of the source indices. Defaults to a #pod temporary directory if not specified. #pod #pod =cut sub BUILD { my $self = shift; # cache directory needs to exist my $cache = $self->cache; $cache = File::Temp->newdir unless defined $cache; if ( !-d $cache ) { Carp::croak("Cache directory '$cache' does not exist"); } $self->cache($cache); # ensure mirror URL ends in '/' my $mirror = $self->mirror; $mirror = "http://www.cpan.org/" unless defined $mirror; $mirror =~ s{/?$}{/}; $self->mirror($mirror); return; } my %INDICES = ( mailrc => 'authors/01mailrc.txt.gz', packages => 'modules/02packages.details.txt.gz', ); # XXX refactor out from subs below my %TEST_GENERATORS = ( regexp_nocase => sub { my $arg = shift; my $re = ref $arg eq 'Regexp' ? $arg : qr/\A\Q$arg\E\z/i; return sub { $_[0] =~ $re }; }, regexp => sub { my $arg = shift; my $re = ref $arg eq 'Regexp' ? $arg : qr/\A\Q$arg\E\z/; return sub { $_[0] =~ $re }; }, version => sub { my $arg = shift; my $v = version->parse($arg); return sub { eval { version->parse( $_[0] ) == $v }; }; }, ); my %QUERY_TYPES = ( # package search package => 'regexp', version => 'version', dist => 'regexp', # author search id => 'regexp_nocase', # XXX need to add "alias " first fullname => 'regexp_nocase', email => 'regexp_nocase', ); sub cached_package { my ($self) = @_; my $package = File::Spec->catfile( $self->cache, File::Basename::basename( $INDICES{packages} ) ); $package =~ s/\.gz$//; $self->refresh_index unless -r $package; return $package; } sub cached_mailrc { my ($self) = @_; my $mailrc = File::Spec->catfile( $self->cache, File::Basename::basename( $INDICES{mailrc} ) ); $mailrc =~ s/\.gz$//; $self->refresh_index unless -r $mailrc; return $mailrc; } sub refresh_index { my ($self) = @_; for my $file ( values %INDICES ) { my $remote = URI->new_abs( $file, $self->mirror ); $remote =~ s/\.gz$// unless $HAS_IO_UNCOMPRESS_GUNZIP; my $ff = File::Fetch->new( uri => $remote ); my $where = $ff->fetch( to => $self->cache ) or Carp::croak( $ff->error ); if ($HAS_IO_UNCOMPRESS_GUNZIP) { ( my $uncompressed = $where ) =~ s/\.gz$//; no warnings 'once'; IO::Uncompress::Gunzip::gunzip( $where, $uncompressed ) or Carp::croak "gunzip failed: $IO::Uncompress::Gunzip::GunzipError\n"; } } return 1; } # epoch secs sub index_age { my ($self) = @_; my $package = $self->cached_package; return ( -r $package ? ( stat($package) )[9] : 0 ); # mtime if readable } sub search_packages { my ( $self, $args ) = @_; Carp::croak("Argument to search_packages must be hash reference") unless ref $args eq 'HASH'; my $index_path = $self->cached_package; die "Can't read $index_path" unless -r $index_path; my $fh = IO::Handle->new; tie *$fh, 'Tie::Handle::SkipHeader', "<", $index_path or die "Can't tie $index_path: $!"; # Convert scalars or regexps to subs my $rules; while ( my ( $k, $v ) = each %$args ) { $rules->{$k} = _rulify( $k, $v ); } my @found; if ( $args->{package} and ref $args->{package} eq '' ) { # binary search 02packages on package my $pos = look $fh, $args->{package}, { xfrm => \&_xform_package, fold => 1 }; return if $pos == -1; # loop over any case-insensitive matching lines LINE: while ( my $line = <$fh> ) { last unless $line =~ /\A\Q$args->{package}\E\s+/i; push @found, _match_package_line( $line, $rules ); } } else { # iterate all lines looking for match LINE: while ( my $line = <$fh> ) { push @found, _match_package_line( $line, $rules ); } } return wantarray ? @found : $found[0]; } sub search_authors { my ( $self, $args ) = @_; Carp::croak("Argument to search_authors must be hash reference") unless ref $args eq 'HASH'; my $index_path = $self->cached_mailrc; die "Can't read $index_path" unless -r $index_path; open my $fh, $index_path or die "Can't open $index_path: $!"; # Convert scalars or regexps to subs my $rules; while ( my ( $k, $v ) = each %$args ) { $rules->{$k} = _rulify( $k, $v ); } my @found; if ( $args->{id} and ref $args->{id} eq '' ) { # binary search mailrec on package my $pos = look $fh, $args->{id}, { xfrm => \&_xform_mailrc, fold => 1 }; return if $pos == -1; my $line = <$fh>; push @found, _match_mailrc_line( $line, $rules ); } else { # iterate all lines looking for match LINE: while ( my $line = <$fh> ) { push @found, _match_mailrc_line( $line, $rules ); } } return wantarray ? @found : $found[0]; } sub _rulify { my ( $key, $arg ) = @_; return $arg if ref($arg) eq 'CODE'; return $TEST_GENERATORS{ $QUERY_TYPES{$key} }->($arg); } sub _xform_package { my @fields = split " ", $_[0], 2; return $fields[0]; } sub _xform_mailrc { my @fields = split " ", $_[0], 3; return $fields[1]; } sub _match_package_line { my ( $line, $rules ) = @_; return unless defined $line; my ( $mod, $version, $dist, $comment ) = split " ", $line, 4; if ( $rules->{package} ) { return unless $rules->{package}->($mod); } if ( $rules->{version} ) { return unless $rules->{version}->($version); } if ( $rules->{dist} ) { return unless $rules->{dist}->($dist); } $dist =~ s{\A./../}{}; return { package => $mod, version => $version, uri => "cpan:///distfile/$dist", }; } sub _match_mailrc_line { my ( $line, $rules ) = @_; return unless defined $line; my ( $id, $address ) = $line =~ m{\Aalias\s+(\S+)\s+"(.*)"}; my ( $fullname, $email ) = $address =~ m{([^<]+)<([^>]+)>}; $fullname =~ s/\s*$//; if ( $rules->{id} ) { return unless $rules->{id}->($id); } if ( $rules->{fullname} ) { return unless $rules->{fullname}->($fullname); } if ( $rules->{email} ) { return unless $rules->{email}->($email); } return { id => $id, fullname => $fullname, email => $email, }; } 1; # vim: ts=4 sts=4 sw=4 et: __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Common::Index::Mirror - Search index via CPAN mirror flatfiles =head1 VERSION version 0.010 =head1 SYNOPSIS use CPAN::Common::Index::Mirror; # default mirror is http://www.cpan.org/ $index = CPAN::Common::Index::Mirror->new; # custom mirror $index = CPAN::Common::Index::Mirror->new( { mirror => "http://cpan.cpantesters.org" } ); =head1 DESCRIPTION This module implements a CPAN::Common::Index that retrieves and searches 02packages.details.txt and 01mailrc.txt indices. The default mirror is L. This is a globally balanced fast mirror and is a great choice if you don't have a local fast mirror. =head1 ATTRIBUTES =head2 mirror URI to a CPAN mirror. Defaults to C. =head2 cache Path to a local directory to store copies of the source indices. Defaults to a temporary directory if not specified. =for Pod::Coverage attributes validate_attributes search_packages search_authors cached_package cached_mailrc BUILD =head1 AUTHOR David Golden =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2013 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut CPAN_COMMON_INDEX_MIRROR $fatpacked{"CPAN/Common/Index/Mux/Ordered.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_COMMON_INDEX_MUX_ORDERED'; use 5.008001; use strict; use warnings; package CPAN::Common::Index::Mux::Ordered; # ABSTRACT: Consult indices in order and return the first result our $VERSION = '0.010'; use parent 'CPAN::Common::Index'; use Class::Tiny qw/resolvers/; use Module::Load (); #pod =attr resolvers #pod #pod An array reference of CPAN::Common::Index::* objects #pod #pod =cut sub BUILD { my $self = shift; my $resolvers = $self->resolvers; $resolvers = [] unless defined $resolvers; if ( ref $resolvers ne 'ARRAY' ) { Carp::croak("The 'resolvers' argument must be an array reference"); } for my $r (@$resolvers) { if ( !eval { $r->isa("CPAN::Common::Index") } ) { Carp::croak("Resolver '$r' is not a CPAN::Common::Index object"); } } $self->resolvers($resolvers); return; } #pod =method assemble #pod #pod $index = CPAN::Common::Index::Mux::Ordered->assemble( #pod MetaDB => {}, #pod Mirror => { mirror => "http://www.cpan.org" }, #pod ); #pod #pod This class method provides a shorthand for constructing a multiplexer. #pod The arguments must be pairs of subclass suffixes and arguments. For #pod example, "MetaDB" means to use "CPAN::Common::Index::MetaDB". Empty #pod arguments must be given as an empty hash reference. #pod #pod =cut sub assemble { my ( $class, @backends ) = @_; my @resolvers; while (@backends) { my ( $subclass, $config ) = splice @backends, 0, 2; my $full_class = "CPAN::Common::Index::${subclass}"; eval { Module::Load::load($full_class); 1 } or Carp::croak($@); my $object = $full_class->new($config); push @resolvers, $object; } return $class->new( { resolvers => \@resolvers } ); } sub validate_attributes { my ($self) = @_; my $resolvers = $self->resolvers; return 1; } # have to think carefully about the sematics of regex search when indices # are stacked; only one result for any given package (or package/version) sub search_packages { my ( $self, $args ) = @_; Carp::croak("Argument to search_packages must be hash reference") unless ref $args eq 'HASH'; my @found; if ( $args->{name} and ref $args->{name} eq '' ) { # looking for exact match, so we just want the first hit for my $source ( @{ $self->resolvers } ) { if ( my @result = $source->search_packages($args) ) { # XXX double check against remaining $args push @found, @result; last; } } } else { # accumulate results from all resolvers my %seen; for my $source ( @{ $self->resolvers } ) { my @result = $source->search_packages($args); push @found, grep { !$seen{ $_->{package} }++ } @result; } } return wantarray ? @found : $found[0]; } # have to think carefully about the sematics of regex search when indices # are stacked; only one result for any given package (or package/version) sub search_authors { my ( $self, $args ) = @_; Carp::croak("Argument to search_authors must be hash reference") unless ref $args eq 'HASH'; my @found; if ( $args->{name} and ref $args->{name} eq '' ) { # looking for exact match, so we just want the first hit for my $source ( @{ $self->resolvers } ) { if ( my @result = $source->search_authors($args) ) { # XXX double check against remaining $args push @found, @result; last; } } } else { # accumulate results from all resolvers my %seen; for my $source ( @{ $self->resolvers } ) { my @result = $source->search_authors($args); push @found, grep { !$seen{ $_->{package} }++ } @result; } } return wantarray ? @found : $found[0]; } 1; # vim: ts=4 sts=4 sw=4 et: __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Common::Index::Mux::Ordered - Consult indices in order and return the first result =head1 VERSION version 0.010 =head1 SYNOPSIS use CPAN::Common::Index::Mux::Ordered; use Data::Dumper; $index = CPAN::Common::Index::Mux::Ordered->assemble( MetaDB => {}, Mirror => { mirror => "http://cpan.cpantesters.org" }, ); =head1 DESCRIPTION This module multiplexes multiple CPAN::Common::Index objects, returning results in order. For exact match queries, the first result is returned. For search queries, results from each index object are concatenated. =head1 ATTRIBUTES =head2 resolvers An array reference of CPAN::Common::Index::* objects =head1 METHODS =head2 assemble $index = CPAN::Common::Index::Mux::Ordered->assemble( MetaDB => {}, Mirror => { mirror => "http://www.cpan.org" }, ); This class method provides a shorthand for constructing a multiplexer. The arguments must be pairs of subclass suffixes and arguments. For example, "MetaDB" means to use "CPAN::Common::Index::MetaDB". Empty arguments must be given as an empty hash reference. =for Pod::Coverage attributes validate_attributes search_packages search_authors BUILD =head1 AUTHOR David Golden =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2013 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut CPAN_COMMON_INDEX_MUX_ORDERED $fatpacked{"CPAN/DistnameInfo.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_DISTNAMEINFO'; package CPAN::DistnameInfo; $VERSION = "0.12"; use strict; sub distname_info { my $file = shift or return; my ($dist, $version) = $file =~ /^ ((?:[-+.]*(?:[A-Za-z0-9]+|(?<=\D)_|_(?=\D))* (?: [A-Za-z](?=[^A-Za-z]|$) | \d(?=-) )(? 6 and $1 & 1) or ($2 and $2 >= 50)) or $3; } elsif ($version =~ /\d\D\d+_\d/ or $version =~ /-TRIAL/) { $dev = 1; } } else { $version = undef; } ($dist, $version, $dev); } sub new { my $class = shift; my $distfile = shift; $distfile =~ s,//+,/,g; my %info = ( pathname => $distfile ); ($info{filename} = $distfile) =~ s,^(((.*?/)?authors/)?id/)?([A-Z])/(\4[A-Z])/(\5[-A-Z0-9]*)/,, and $info{cpanid} = $6; if ($distfile =~ m,([^/]+)\.(tar\.(?:g?z|bz2)|zip|tgz)$,i) { # support more ? $info{distvname} = $1; $info{extension} = $2; } @info{qw(dist version beta)} = distname_info($info{distvname}); $info{maturity} = delete $info{beta} ? 'developer' : 'released'; return bless \%info, $class; } sub dist { shift->{dist} } sub version { shift->{version} } sub maturity { shift->{maturity} } sub filename { shift->{filename} } sub cpanid { shift->{cpanid} } sub distvname { shift->{distvname} } sub extension { shift->{extension} } sub pathname { shift->{pathname} } sub properties { %{ $_[0] } } 1; __END__ =head1 NAME CPAN::DistnameInfo - Extract distribution name and version from a distribution filename =head1 SYNOPSIS my $pathname = "authors/id/G/GB/GBARR/CPAN-DistnameInfo-0.02.tar.gz"; my $d = CPAN::DistnameInfo->new($pathname); my $dist = $d->dist; # "CPAN-DistnameInfo" my $version = $d->version; # "0.02" my $maturity = $d->maturity; # "released" my $filename = $d->filename; # "CPAN-DistnameInfo-0.02.tar.gz" my $cpanid = $d->cpanid; # "GBARR" my $distvname = $d->distvname; # "CPAN-DistnameInfo-0.02" my $extension = $d->extension; # "tar.gz" my $pathname = $d->pathname; # "authors/id/G/GB/GBARR/..." my %prop = $d->properties; =head1 DESCRIPTION Many online services that are centered around CPAN attempt to associate multiple uploads by extracting a distribution name from the filename of the upload. For most distributions this is easy as they have used ExtUtils::MakeMaker or Module::Build to create the distribution, which results in a uniform name. But sadly not all uploads are created in this way. C uses heuristics that have been learnt by L to extract the distribution name and version from filenames and also report if the version is to be treated as a developer release The constructor takes a single pathname, returning an object with the following methods =over =item cpanid If the path given looked like a CPAN authors directory path, then this will be the the CPAN id of the author. =item dist The name of the distribution =item distvname The file name with any suffix and leading directory names removed =item filename If the path given looked like a CPAN authors directory path, then this will be the path to the file relative to the detected CPAN author directory. Otherwise it is the path that was passed in. =item maturity The maturity of the distribution. This will be either C or C =item extension The extension of the distribution, often used to denote the archive type (e.g. 'tar.gz') =item pathname The pathname that was passed to the constructor when creating the object. =item properties This will return a list of key-value pairs, suitable for assigning to a hash, for the known properties. =item version The extracted version =back =head1 AUTHOR Graham Barr =head1 COPYRIGHT Copyright (c) 2003 Graham Barr. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut CPAN_DISTNAMEINFO $fatpacked{"CPAN/Meta.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META'; use 5.006; use strict; use warnings; package CPAN::Meta; # VERSION $CPAN::Meta::VERSION = '2.143240'; #pod =head1 SYNOPSIS #pod #pod use v5.10; #pod use strict; #pod use warnings; #pod use CPAN::Meta; #pod use Module::Load; #pod #pod my $meta = CPAN::Meta->load_file('META.json'); #pod #pod printf "testing requirements for %s version %s\n", #pod $meta->name, #pod $meta->version; #pod #pod my $prereqs = $meta->effective_prereqs; #pod #pod for my $phase ( qw/configure runtime build test/ ) { #pod say "Requirements for $phase:"; #pod my $reqs = $prereqs->requirements_for($phase, "requires"); #pod for my $module ( sort $reqs->required_modules ) { #pod my $status; #pod if ( eval { load $module unless $module eq 'perl'; 1 } ) { #pod my $version = $module eq 'perl' ? $] : $module->VERSION; #pod $status = $reqs->accepts_module($module, $version) #pod ? "$version ok" : "$version not ok"; #pod } else { #pod $status = "missing" #pod }; #pod say " $module ($status)"; #pod } #pod } #pod #pod =head1 DESCRIPTION #pod #pod Software distributions released to the CPAN include a F or, for #pod older distributions, F, which describes the distribution, its #pod contents, and the requirements for building and installing the distribution. #pod The data structure stored in the F file is described in #pod L. #pod #pod CPAN::Meta provides a simple class to represent this distribution metadata (or #pod I), along with some helpful methods for interrogating that data. #pod #pod The documentation below is only for the methods of the CPAN::Meta object. For #pod information on the meaning of individual fields, consult the spec. #pod #pod =cut use Carp qw(carp croak); use CPAN::Meta::Feature; use CPAN::Meta::Prereqs; use CPAN::Meta::Converter; use CPAN::Meta::Validator; use Parse::CPAN::Meta 1.4414 (); BEGIN { *_dclone = \&CPAN::Meta::Converter::_dclone } #pod =head1 STRING DATA #pod #pod The following methods return a single value, which is the value for the #pod corresponding entry in the distmeta structure. Values should be either undef #pod or strings. #pod #pod =for :list #pod * abstract #pod * description #pod * dynamic_config #pod * generated_by #pod * name #pod * release_status #pod * version #pod #pod =cut BEGIN { my @STRING_READERS = qw( abstract description dynamic_config generated_by name release_status version ); no strict 'refs'; for my $attr (@STRING_READERS) { *$attr = sub { $_[0]{ $attr } }; } } #pod =head1 LIST DATA #pod #pod These methods return lists of string values, which might be represented in the #pod distmeta structure as arrayrefs or scalars: #pod #pod =for :list #pod * authors #pod * keywords #pod * licenses #pod #pod The C and C methods may also be called as C and #pod C, respectively, to match the field name in the distmeta structure. #pod #pod =cut BEGIN { my @LIST_READERS = qw( author keywords license ); no strict 'refs'; for my $attr (@LIST_READERS) { *$attr = sub { my $value = $_[0]{ $attr }; croak "$attr must be called in list context" unless wantarray; return @{ _dclone($value) } if ref $value; return $value; }; } } sub authors { $_[0]->author } sub licenses { $_[0]->license } #pod =head1 MAP DATA #pod #pod These readers return hashrefs of arbitrary unblessed data structures, each #pod described more fully in the specification: #pod #pod =for :list #pod * meta_spec #pod * resources #pod * provides #pod * no_index #pod * prereqs #pod * optional_features #pod #pod =cut BEGIN { my @MAP_READERS = qw( meta-spec resources provides no_index prereqs optional_features ); no strict 'refs'; for my $attr (@MAP_READERS) { (my $subname = $attr) =~ s/-/_/; *$subname = sub { my $value = $_[0]{ $attr }; return _dclone($value) if $value; return {}; }; } } #pod =head1 CUSTOM DATA #pod #pod A list of custom keys are available from the C method and #pod particular keys may be retrieved with the C method. #pod #pod say $meta->custom($_) for $meta->custom_keys; #pod #pod If a custom key refers to a data structure, a deep clone is returned. #pod #pod =cut sub custom_keys { return grep { /^x_/i } keys %{$_[0]}; } sub custom { my ($self, $attr) = @_; my $value = $self->{$attr}; return _dclone($value) if ref $value; return $value; } #pod =method new #pod #pod my $meta = CPAN::Meta->new($distmeta_struct, \%options); #pod #pod Returns a valid CPAN::Meta object or dies if the supplied metadata hash #pod reference fails to validate. Older-format metadata will be up-converted to #pod version 2 if they validate against the original stated specification. #pod #pod It takes an optional hashref of options. Valid options include: #pod #pod =over #pod #pod =item * #pod #pod lazy_validation -- if true, new will attempt to convert the given metadata #pod to version 2 before attempting to validate it. This means than any #pod fixable errors will be handled by CPAN::Meta::Converter before validation. #pod (Note that this might result in invalid optional data being silently #pod dropped.) The default is false. #pod #pod =back #pod #pod =cut sub _new { my ($class, $struct, $options) = @_; my $self; if ( $options->{lazy_validation} ) { # try to convert to a valid structure; if succeeds, then return it my $cmc = CPAN::Meta::Converter->new( $struct ); $self = $cmc->convert( version => 2 ); # valid or dies return bless $self, $class; } else { # validate original struct my $cmv = CPAN::Meta::Validator->new( $struct ); unless ( $cmv->is_valid) { die "Invalid metadata structure. Errors: " . join(", ", $cmv->errors) . "\n"; } } # up-convert older spec versions my $version = $struct->{'meta-spec'}{version} || '1.0'; if ( $version == 2 ) { $self = $struct; } else { my $cmc = CPAN::Meta::Converter->new( $struct ); $self = $cmc->convert( version => 2 ); } return bless $self, $class; } sub new { my ($class, $struct, $options) = @_; my $self = eval { $class->_new($struct, $options) }; croak($@) if $@; return $self; } #pod =method create #pod #pod my $meta = CPAN::Meta->create($distmeta_struct, \%options); #pod #pod This is same as C, except that C and C fields #pod will be generated if not provided. This means the metadata structure is #pod assumed to otherwise follow the latest L. #pod #pod =cut sub create { my ($class, $struct, $options) = @_; my $version = __PACKAGE__->VERSION || 2; $struct->{generated_by} ||= __PACKAGE__ . " version $version" ; $struct->{'meta-spec'}{version} ||= int($version); my $self = eval { $class->_new($struct, $options) }; croak ($@) if $@; return $self; } #pod =method load_file #pod #pod my $meta = CPAN::Meta->load_file($distmeta_file, \%options); #pod #pod Given a pathname to a file containing metadata, this deserializes the file #pod according to its file suffix and constructs a new C object, just #pod like C. It will die if the deserialized version fails to validate #pod against its stated specification version. #pod #pod It takes the same options as C but C defaults to #pod true. #pod #pod =cut sub load_file { my ($class, $file, $options) = @_; $options->{lazy_validation} = 1 unless exists $options->{lazy_validation}; croak "load_file() requires a valid, readable filename" unless -r $file; my $self; eval { my $struct = Parse::CPAN::Meta->load_file( $file ); $self = $class->_new($struct, $options); }; croak($@) if $@; return $self; } #pod =method load_yaml_string #pod #pod my $meta = CPAN::Meta->load_yaml_string($yaml, \%options); #pod #pod This method returns a new CPAN::Meta object using the first document in the #pod given YAML string. In other respects it is identical to C. #pod #pod =cut sub load_yaml_string { my ($class, $yaml, $options) = @_; $options->{lazy_validation} = 1 unless exists $options->{lazy_validation}; my $self; eval { my ($struct) = Parse::CPAN::Meta->load_yaml_string( $yaml ); $self = $class->_new($struct, $options); }; croak($@) if $@; return $self; } #pod =method load_json_string #pod #pod my $meta = CPAN::Meta->load_json_string($json, \%options); #pod #pod This method returns a new CPAN::Meta object using the structure represented by #pod the given JSON string. In other respects it is identical to C. #pod #pod =cut sub load_json_string { my ($class, $json, $options) = @_; $options->{lazy_validation} = 1 unless exists $options->{lazy_validation}; my $self; eval { my $struct = Parse::CPAN::Meta->load_json_string( $json ); $self = $class->_new($struct, $options); }; croak($@) if $@; return $self; } #pod =method load_string #pod #pod my $meta = CPAN::Meta->load_string($string, \%options); #pod #pod If you don't know if a string contains YAML or JSON, this method will use #pod L to guess. In other respects it is identical to #pod C. #pod #pod =cut sub load_string { my ($class, $string, $options) = @_; $options->{lazy_validation} = 1 unless exists $options->{lazy_validation}; my $self; eval { my $struct = Parse::CPAN::Meta->load_string( $string ); $self = $class->_new($struct, $options); }; croak($@) if $@; return $self; } #pod =method save #pod #pod $meta->save($distmeta_file, \%options); #pod #pod Serializes the object as JSON and writes it to the given file. The only valid #pod option is C, which defaults to '2'. On Perl 5.8.1 or later, the file #pod is saved with UTF-8 encoding. #pod #pod For C 2 (or higher), the filename should end in '.json'. L #pod is the default JSON backend. Using another JSON backend requires L 2.5 or #pod later and you must set the C<$ENV{PERL_JSON_BACKEND}> to a supported alternate #pod backend like L. #pod #pod For C less than 2, the filename should end in '.yml'. #pod L is used to generate an older metadata structure, which #pod is serialized to YAML. CPAN::Meta::YAML is the default YAML backend. You may #pod set the C<$ENV{PERL_YAML_BACKEND}> to a supported alternative backend, though #pod this is not recommended due to subtle incompatibilities between YAML parsers on #pod CPAN. #pod #pod =cut sub save { my ($self, $file, $options) = @_; my $version = $options->{version} || '2'; my $layer = $] ge '5.008001' ? ':utf8' : ''; if ( $version ge '2' ) { carp "'$file' should end in '.json'" unless $file =~ m{\.json$}; } else { carp "'$file' should end in '.yml'" unless $file =~ m{\.yml$}; } my $data = $self->as_string( $options ); open my $fh, ">$layer", $file or die "Error opening '$file' for writing: $!\n"; print {$fh} $data; close $fh or die "Error closing '$file': $!\n"; return 1; } #pod =method meta_spec_version #pod #pod This method returns the version part of the C entry in the distmeta #pod structure. It is equivalent to: #pod #pod $meta->meta_spec->{version}; #pod #pod =cut sub meta_spec_version { my ($self) = @_; return $self->meta_spec->{version}; } #pod =method effective_prereqs #pod #pod my $prereqs = $meta->effective_prereqs; #pod #pod my $prereqs = $meta->effective_prereqs( \@feature_identifiers ); #pod #pod This method returns a L object describing all the #pod prereqs for the distribution. If an arrayref of feature identifiers is given, #pod the prereqs for the identified features are merged together with the #pod distribution's core prereqs before the CPAN::Meta::Prereqs object is returned. #pod #pod =cut sub effective_prereqs { my ($self, $features) = @_; $features ||= []; my $prereq = CPAN::Meta::Prereqs->new($self->prereqs); return $prereq unless @$features; my @other = map {; $self->feature($_)->prereqs } @$features; return $prereq->with_merged_prereqs(\@other); } #pod =method should_index_file #pod #pod ... if $meta->should_index_file( $filename ); #pod #pod This method returns true if the given file should be indexed. It decides this #pod by checking the C and C keys in the C property of #pod the distmeta structure. Note that neither the version format nor #pod C are considered. #pod #pod C<$filename> should be given in unix format. #pod #pod =cut sub should_index_file { my ($self, $filename) = @_; for my $no_index_file (@{ $self->no_index->{file} || [] }) { return if $filename eq $no_index_file; } for my $no_index_dir (@{ $self->no_index->{directory} }) { $no_index_dir =~ s{$}{/} unless $no_index_dir =~ m{/\z}; return if index($filename, $no_index_dir) == 0; } return 1; } #pod =method should_index_package #pod #pod ... if $meta->should_index_package( $package ); #pod #pod This method returns true if the given package should be indexed. It decides #pod this by checking the C and C keys in the C #pod property of the distmeta structure. Note that neither the version format nor #pod C are considered. #pod #pod =cut sub should_index_package { my ($self, $package) = @_; for my $no_index_pkg (@{ $self->no_index->{package} || [] }) { return if $package eq $no_index_pkg; } for my $no_index_ns (@{ $self->no_index->{namespace} }) { return if index($package, "${no_index_ns}::") == 0; } return 1; } #pod =method features #pod #pod my @feature_objects = $meta->features; #pod #pod This method returns a list of L objects, one for each #pod optional feature described by the distribution's metadata. #pod #pod =cut sub features { my ($self) = @_; my $opt_f = $self->optional_features; my @features = map {; CPAN::Meta::Feature->new($_ => $opt_f->{ $_ }) } keys %$opt_f; return @features; } #pod =method feature #pod #pod my $feature_object = $meta->feature( $identifier ); #pod #pod This method returns a L object for the optional feature #pod with the given identifier. If no feature with that identifier exists, an #pod exception will be raised. #pod #pod =cut sub feature { my ($self, $ident) = @_; croak "no feature named $ident" unless my $f = $self->optional_features->{ $ident }; return CPAN::Meta::Feature->new($ident, $f); } #pod =method as_struct #pod #pod my $copy = $meta->as_struct( \%options ); #pod #pod This method returns a deep copy of the object's metadata as an unblessed hash #pod reference. It takes an optional hashref of options. If the hashref contains #pod a C argument, the copied metadata will be converted to the version #pod of the specification and returned. For example: #pod #pod my $old_spec = $meta->as_struct( {version => "1.4"} ); #pod #pod =cut sub as_struct { my ($self, $options) = @_; my $struct = _dclone($self); if ( $options->{version} ) { my $cmc = CPAN::Meta::Converter->new( $struct ); $struct = $cmc->convert( version => $options->{version} ); } return $struct; } #pod =method as_string #pod #pod my $string = $meta->as_string( \%options ); #pod #pod This method returns a serialized copy of the object's metadata as a character #pod string. (The strings are B UTF-8 encoded.) It takes an optional hashref #pod of options. If the hashref contains a C argument, the copied metadata #pod will be converted to the version of the specification and returned. For #pod example: #pod #pod my $string = $meta->as_string( {version => "1.4"} ); #pod #pod For C greater than or equal to 2, the string will be serialized as #pod JSON. For C less than 2, the string will be serialized as YAML. In #pod both cases, the same rules are followed as in the C method for choosing #pod a serialization backend. #pod #pod =cut sub as_string { my ($self, $options) = @_; my $version = $options->{version} || '2'; my $struct; if ( $self->meta_spec_version ne $version ) { my $cmc = CPAN::Meta::Converter->new( $self->as_struct ); $struct = $cmc->convert( version => $version ); } else { $struct = $self->as_struct; } my ($data, $backend); if ( $version ge '2' ) { $backend = Parse::CPAN::Meta->json_backend(); $data = $backend->new->pretty->canonical->encode($struct); } else { $backend = Parse::CPAN::Meta->yaml_backend(); $data = eval { no strict 'refs'; &{"$backend\::Dump"}($struct) }; if ( $@ ) { croak $backend->can('errstr') ? $backend->errstr : $@ } } return $data; } # Used by JSON::PP, etc. for "convert_blessed" sub TO_JSON { return { %{ $_[0] } }; } 1; # ABSTRACT: the distribution metadata for a CPAN dist __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta - the distribution metadata for a CPAN dist =head1 VERSION version 2.143240 =head1 SYNOPSIS use v5.10; use strict; use warnings; use CPAN::Meta; use Module::Load; my $meta = CPAN::Meta->load_file('META.json'); printf "testing requirements for %s version %s\n", $meta->name, $meta->version; my $prereqs = $meta->effective_prereqs; for my $phase ( qw/configure runtime build test/ ) { say "Requirements for $phase:"; my $reqs = $prereqs->requirements_for($phase, "requires"); for my $module ( sort $reqs->required_modules ) { my $status; if ( eval { load $module unless $module eq 'perl'; 1 } ) { my $version = $module eq 'perl' ? $] : $module->VERSION; $status = $reqs->accepts_module($module, $version) ? "$version ok" : "$version not ok"; } else { $status = "missing" }; say " $module ($status)"; } } =head1 DESCRIPTION Software distributions released to the CPAN include a F or, for older distributions, F, which describes the distribution, its contents, and the requirements for building and installing the distribution. The data structure stored in the F file is described in L. CPAN::Meta provides a simple class to represent this distribution metadata (or I), along with some helpful methods for interrogating that data. The documentation below is only for the methods of the CPAN::Meta object. For information on the meaning of individual fields, consult the spec. =head1 METHODS =head2 new my $meta = CPAN::Meta->new($distmeta_struct, \%options); Returns a valid CPAN::Meta object or dies if the supplied metadata hash reference fails to validate. Older-format metadata will be up-converted to version 2 if they validate against the original stated specification. It takes an optional hashref of options. Valid options include: =over =item * lazy_validation -- if true, new will attempt to convert the given metadata to version 2 before attempting to validate it. This means than any fixable errors will be handled by CPAN::Meta::Converter before validation. (Note that this might result in invalid optional data being silently dropped.) The default is false. =back =head2 create my $meta = CPAN::Meta->create($distmeta_struct, \%options); This is same as C, except that C and C fields will be generated if not provided. This means the metadata structure is assumed to otherwise follow the latest L. =head2 load_file my $meta = CPAN::Meta->load_file($distmeta_file, \%options); Given a pathname to a file containing metadata, this deserializes the file according to its file suffix and constructs a new C object, just like C. It will die if the deserialized version fails to validate against its stated specification version. It takes the same options as C but C defaults to true. =head2 load_yaml_string my $meta = CPAN::Meta->load_yaml_string($yaml, \%options); This method returns a new CPAN::Meta object using the first document in the given YAML string. In other respects it is identical to C. =head2 load_json_string my $meta = CPAN::Meta->load_json_string($json, \%options); This method returns a new CPAN::Meta object using the structure represented by the given JSON string. In other respects it is identical to C. =head2 load_string my $meta = CPAN::Meta->load_string($string, \%options); If you don't know if a string contains YAML or JSON, this method will use L to guess. In other respects it is identical to C. =head2 save $meta->save($distmeta_file, \%options); Serializes the object as JSON and writes it to the given file. The only valid option is C, which defaults to '2'. On Perl 5.8.1 or later, the file is saved with UTF-8 encoding. For C 2 (or higher), the filename should end in '.json'. L is the default JSON backend. Using another JSON backend requires L 2.5 or later and you must set the C<$ENV{PERL_JSON_BACKEND}> to a supported alternate backend like L. For C less than 2, the filename should end in '.yml'. L is used to generate an older metadata structure, which is serialized to YAML. CPAN::Meta::YAML is the default YAML backend. You may set the C<$ENV{PERL_YAML_BACKEND}> to a supported alternative backend, though this is not recommended due to subtle incompatibilities between YAML parsers on CPAN. =head2 meta_spec_version This method returns the version part of the C entry in the distmeta structure. It is equivalent to: $meta->meta_spec->{version}; =head2 effective_prereqs my $prereqs = $meta->effective_prereqs; my $prereqs = $meta->effective_prereqs( \@feature_identifiers ); This method returns a L object describing all the prereqs for the distribution. If an arrayref of feature identifiers is given, the prereqs for the identified features are merged together with the distribution's core prereqs before the CPAN::Meta::Prereqs object is returned. =head2 should_index_file ... if $meta->should_index_file( $filename ); This method returns true if the given file should be indexed. It decides this by checking the C and C keys in the C property of the distmeta structure. Note that neither the version format nor C are considered. C<$filename> should be given in unix format. =head2 should_index_package ... if $meta->should_index_package( $package ); This method returns true if the given package should be indexed. It decides this by checking the C and C keys in the C property of the distmeta structure. Note that neither the version format nor C are considered. =head2 features my @feature_objects = $meta->features; This method returns a list of L objects, one for each optional feature described by the distribution's metadata. =head2 feature my $feature_object = $meta->feature( $identifier ); This method returns a L object for the optional feature with the given identifier. If no feature with that identifier exists, an exception will be raised. =head2 as_struct my $copy = $meta->as_struct( \%options ); This method returns a deep copy of the object's metadata as an unblessed hash reference. It takes an optional hashref of options. If the hashref contains a C argument, the copied metadata will be converted to the version of the specification and returned. For example: my $old_spec = $meta->as_struct( {version => "1.4"} ); =head2 as_string my $string = $meta->as_string( \%options ); This method returns a serialized copy of the object's metadata as a character string. (The strings are B UTF-8 encoded.) It takes an optional hashref of options. If the hashref contains a C argument, the copied metadata will be converted to the version of the specification and returned. For example: my $string = $meta->as_string( {version => "1.4"} ); For C greater than or equal to 2, the string will be serialized as JSON. For C less than 2, the string will be serialized as YAML. In both cases, the same rules are followed as in the C method for choosing a serialization backend. =head1 STRING DATA The following methods return a single value, which is the value for the corresponding entry in the distmeta structure. Values should be either undef or strings. =over 4 =item * abstract =item * description =item * dynamic_config =item * generated_by =item * name =item * release_status =item * version =back =head1 LIST DATA These methods return lists of string values, which might be represented in the distmeta structure as arrayrefs or scalars: =over 4 =item * authors =item * keywords =item * licenses =back The C and C methods may also be called as C and C, respectively, to match the field name in the distmeta structure. =head1 MAP DATA These readers return hashrefs of arbitrary unblessed data structures, each described more fully in the specification: =over 4 =item * meta_spec =item * resources =item * provides =item * no_index =item * prereqs =item * optional_features =back =head1 CUSTOM DATA A list of custom keys are available from the C method and particular keys may be retrieved with the C method. say $meta->custom($_) for $meta->custom_keys; If a custom key refers to a data structure, a deep clone is returned. =for Pod::Coverage TO_JSON abstract author authors custom custom_keys description dynamic_config generated_by keywords license licenses meta_spec name no_index optional_features prereqs provides release_status resources version =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 SEE ALSO =over 4 =item * L =item * L =back =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/Perl-Toolchain-Gang/CPAN-Meta.git =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 CONTRIBUTORS =for stopwords Ansgar Burchardt Avar Arnfjord Bjarmason Christopher J. Madsen Chuck Adams Cory G Watson Damyan Ivanov Eric Wilhelm Graham Knop Gregor Hermann Karen Etheridge Kenichi Ishigaki Ken Williams Lars Dieckow Leon Timmermans majensen Mark Fowler Matt S Trout Michael G. Schwern moznion Olaf Alders Olivier Mengue Randy Sims =over 4 =item * Ansgar Burchardt =item * Avar Arnfjord Bjarmason =item * Christopher J. Madsen =item * Chuck Adams =item * Cory G Watson =item * Damyan Ivanov =item * Eric Wilhelm =item * Graham Knop =item * Gregor Hermann =item * Karen Etheridge =item * Kenichi Ishigaki =item * Ken Williams =item * Lars Dieckow =item * Leon Timmermans =item * majensen =item * Mark Fowler =item * Matt S Trout =item * Michael G. Schwern =item * moznion =item * Olaf Alders =item * Olivier Mengue =item * Randy Sims =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META $fatpacked{"CPAN/Meta/Check.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_CHECK'; package CPAN::Meta::Check; $CPAN::Meta::Check::VERSION = '0.014'; use strict; use warnings; use base 'Exporter'; our @EXPORT = qw//; our @EXPORT_OK = qw/check_requirements requirements_for verify_dependencies/; our %EXPORT_TAGS = (all => [ @EXPORT, @EXPORT_OK ] ); use CPAN::Meta::Prereqs '2.132830'; use CPAN::Meta::Requirements 2.121; use Module::Metadata 1.000023; sub _check_dep { my ($reqs, $module, $dirs) = @_; $module eq 'perl' and return ($reqs->accepts_module($module, $]) ? () : sprintf "Your Perl (%s) is not in the range '%s'", $], $reqs->requirements_for_module($module)); my $metadata = Module::Metadata->new_from_module($module, inc => $dirs); return "Module '$module' is not installed" if not defined $metadata; my $version = eval { $metadata->version }; return sprintf 'Installed version (%s) of %s is not in range \'%s\'', (defined $version ? $version : 'undef'), $module, $reqs->requirements_for_module($module) if not $reqs->accepts_module($module, $version || 0); return; } sub _check_conflict { my ($reqs, $module, $dirs) = @_; my $metadata = Module::Metadata->new_from_module($module, inc => $dirs); return if not defined $metadata; my $version = eval { $metadata->version }; return sprintf 'Installed version (%s) of %s is in range \'%s\'', (defined $version ? $version : 'undef'), $module, $reqs->requirements_for_module($module) if $reqs->accepts_module($module, $version); return; } sub requirements_for { my ($meta, $phases, $type) = @_; my $prereqs = ref($meta) eq 'CPAN::Meta' ? $meta->effective_prereqs : $meta; return $prereqs->merged_requirements(ref($phases) ? $phases : [ $phases ], [ $type ]); } sub check_requirements { my ($reqs, $type, $dirs) = @_; return +{ map { $_ => $type ne 'conflicts' ? scalar _check_dep($reqs, $_, $dirs) : scalar _check_conflict($reqs, $_, $dirs) } $reqs->required_modules }; } sub verify_dependencies { my ($meta, $phases, $type, $dirs) = @_; my $reqs = requirements_for($meta, $phases, $type); my $issues = check_requirements($reqs, $type, $dirs); return grep { defined } values %{ $issues }; } 1; #ABSTRACT: Verify requirements in a CPAN::Meta object __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Check - Verify requirements in a CPAN::Meta object =head1 VERSION version 0.014 =head1 SYNOPSIS warn "$_\n" for verify_dependencies($meta, [qw/runtime build test/], 'requires'); =head1 DESCRIPTION This module verifies if requirements described in a CPAN::Meta object are present. =head1 FUNCTIONS =head2 check_requirements($reqs, $type, $incdirs) This function checks if all dependencies in C<$reqs> (a L object) are met, taking into account that 'conflicts' dependencies have to be checked in reverse. It returns a hash with the modules as keys and any problems as values; the value for a successfully found module will be undef. Modules are searched for in C<@$incdirs>, defaulting to C<@INC>. =head2 verify_dependencies($meta, $phases, $types, $incdirs) Check all requirements in C<$meta> for phases C<$phases> and type C<$type>. Modules are searched for in C<@$incdirs>, defaulting to C<@INC>. C<$meta> should be a L or L object. =head2 requirements_for($meta, $phases, $types) B<< This function is deprecated and may be removed at some point in the future, please use CPAN::Meta::Prereqs->merged_requirements instead. >> This function returns a unified L object for all C<$type> requirements for C<$phases>. C<$phases> may be either one (scalar) value or an arrayref of valid values as defined by the L. C<$type> must be a relationship as defined by the same spec. C<$meta> should be a L or L object. =head1 SEE ALSO =over 4 =item * L =item * L =for comment # vi:noet:sts=2:sw=2:ts=2 =back =head1 AUTHOR Leon Timmermans =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2012 by Leon Timmermans. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_CHECK $fatpacked{"CPAN/Meta/Converter.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_CONVERTER'; use 5.006; use strict; use warnings; package CPAN::Meta::Converter; # VERSION $CPAN::Meta::Converter::VERSION = '2.143240'; #pod =head1 SYNOPSIS #pod #pod my $struct = decode_json_file('META.json'); #pod #pod my $cmc = CPAN::Meta::Converter->new( $struct ); #pod #pod my $new_struct = $cmc->convert( version => "2" ); #pod #pod =head1 DESCRIPTION #pod #pod This module converts CPAN Meta structures from one form to another. The #pod primary use is to convert older structures to the most modern version of #pod the specification, but other transformations may be implemented in the #pod future as needed. (E.g. stripping all custom fields or stripping all #pod optional fields.) #pod #pod =cut use CPAN::Meta::Validator; use CPAN::Meta::Requirements; use Parse::CPAN::Meta 1.4400 (); # To help ExtUtils::MakeMaker bootstrap CPAN::Meta::Requirements on perls # before 5.10, we fall back to the EUMM bundled compatibility version module if # that's the only thing available. This shouldn't ever happen in a normal CPAN # install of CPAN::Meta::Requirements, as version.pm will be picked up from # prereqs and be available at runtime. BEGIN { eval "use version ()"; ## no critic if ( my $err = $@ ) { eval "use ExtUtils::MakeMaker::version" or die $err; ## no critic } } # Perl 5.10.0 didn't have "is_qv" in version.pm *_is_qv = version->can('is_qv') ? sub { $_[0]->is_qv } : sub { exists $_[0]->{qv} }; sub _dclone { my $ref = shift; # if an object is in the data structure and doesn't specify how to # turn itself into JSON, we just stringify the object. That does the # right thing for typical things that might be there, like version objects, # Path::Class objects, etc. no warnings 'once'; no warnings 'redefine'; local *UNIVERSAL::TO_JSON = sub { "$_[0]" }; my $json = Parse::CPAN::Meta->json_backend()->new ->utf8 ->allow_blessed ->convert_blessed; $json->decode($json->encode($ref)) } my %known_specs = ( '2' => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec', '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html', '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html', '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html', '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html', '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html' ); my @spec_list = sort { $a <=> $b } keys %known_specs; my ($LOWEST, $HIGHEST) = @spec_list[0,-1]; #--------------------------------------------------------------------------# # converters # # called as $converter->($element, $field_name, $full_meta, $to_version) # # defined return value used for field # undef return value means field is skipped #--------------------------------------------------------------------------# sub _keep { $_[0] } sub _keep_or_one { defined($_[0]) ? $_[0] : 1 } sub _keep_or_zero { defined($_[0]) ? $_[0] : 0 } sub _keep_or_unknown { defined($_[0]) && length($_[0]) ? $_[0] : "unknown" } sub _generated_by { my $gen = shift; my $sig = __PACKAGE__ . " version " . (__PACKAGE__->VERSION || ""); return $sig unless defined $gen and length $gen; return $gen if $gen =~ /\Q$sig/; return "$gen, $sig"; } sub _listify { ! defined $_[0] ? undef : ref $_[0] eq 'ARRAY' ? $_[0] : [$_[0]] } sub _prefix_custom { my $key = shift; $key =~ s/^(?!x_) # Unless it already starts with x_ (?:x-?)? # Remove leading x- or x (if present) /x_/ix; # and prepend x_ return $key; } sub _ucfirst_custom { my $key = shift; $key = ucfirst $key unless $key =~ /[A-Z]/; return $key; } sub _no_prefix_ucfirst_custom { my $key = shift; $key =~ s/^x_//; return _ucfirst_custom($key); } sub _change_meta_spec { my ($element, undef, undef, $version) = @_; return { version => $version, url => $known_specs{$version}, }; } my @open_source = ( 'perl', 'gpl', 'apache', 'artistic', 'artistic_2', 'lgpl', 'bsd', 'gpl', 'mit', 'mozilla', 'open_source', ); my %is_open_source = map {; $_ => 1 } @open_source; my @valid_licenses_1 = ( @open_source, 'unrestricted', 'restrictive', 'unknown', ); my %license_map_1 = ( ( map { $_ => $_ } @valid_licenses_1 ), artistic2 => 'artistic_2', ); sub _license_1 { my ($element) = @_; return 'unknown' unless defined $element; if ( $license_map_1{lc $element} ) { return $license_map_1{lc $element}; } else { return 'unknown'; } } my @valid_licenses_2 = qw( agpl_3 apache_1_1 apache_2_0 artistic_1 artistic_2 bsd freebsd gfdl_1_2 gfdl_1_3 gpl_1 gpl_2 gpl_3 lgpl_2_1 lgpl_3_0 mit mozilla_1_0 mozilla_1_1 openssl perl_5 qpl_1_0 ssleay sun zlib open_source restricted unrestricted unknown ); # The "old" values were defined by Module::Build, and were often vague. I have # made the decisions below based on reading Module::Build::API and how clearly # it specifies the version of the license. my %license_map_2 = ( (map { $_ => $_ } @valid_licenses_2), apache => 'apache_2_0', # clearly stated as 2.0 artistic => 'artistic_1', # clearly stated as 1 artistic2 => 'artistic_2', # clearly stated as 2 gpl => 'open_source', # we don't know which GPL; punt lgpl => 'open_source', # we don't know which LGPL; punt mozilla => 'open_source', # we don't know which MPL; punt perl => 'perl_5', # clearly Perl 5 restrictive => 'restricted', ); sub _license_2 { my ($element) = @_; return [ 'unknown' ] unless defined $element; $element = [ $element ] unless ref $element eq 'ARRAY'; my @new_list; for my $lic ( @$element ) { next unless defined $lic; if ( my $new = $license_map_2{lc $lic} ) { push @new_list, $new; } } return @new_list ? \@new_list : [ 'unknown' ]; } my %license_downgrade_map = qw( agpl_3 open_source apache_1_1 apache apache_2_0 apache artistic_1 artistic artistic_2 artistic_2 bsd bsd freebsd open_source gfdl_1_2 open_source gfdl_1_3 open_source gpl_1 gpl gpl_2 gpl gpl_3 gpl lgpl_2_1 lgpl lgpl_3_0 lgpl mit mit mozilla_1_0 mozilla mozilla_1_1 mozilla openssl open_source perl_5 perl qpl_1_0 open_source ssleay open_source sun open_source zlib open_source open_source open_source restricted restrictive unrestricted unrestricted unknown unknown ); sub _downgrade_license { my ($element) = @_; if ( ! defined $element ) { return "unknown"; } elsif( ref $element eq 'ARRAY' ) { if ( @$element > 1) { if (grep { !$is_open_source{ $license_downgrade_map{lc $_} || 'unknown' } } @$element) { return 'unknown'; } else { return 'open_source'; } } elsif ( @$element == 1 ) { return $license_downgrade_map{lc $element->[0]} || "unknown"; } } elsif ( ! ref $element ) { return $license_downgrade_map{lc $element} || "unknown"; } return "unknown"; } my $no_index_spec_1_2 = { 'file' => \&_listify, 'dir' => \&_listify, 'package' => \&_listify, 'namespace' => \&_listify, }; my $no_index_spec_1_3 = { 'file' => \&_listify, 'directory' => \&_listify, 'package' => \&_listify, 'namespace' => \&_listify, }; my $no_index_spec_2 = { 'file' => \&_listify, 'directory' => \&_listify, 'package' => \&_listify, 'namespace' => \&_listify, ':custom' => \&_prefix_custom, }; sub _no_index_1_2 { my (undef, undef, $meta) = @_; my $no_index = $meta->{no_index} || $meta->{private}; return unless $no_index; # cleanup wrong format if ( ! ref $no_index ) { my $item = $no_index; $no_index = { dir => [ $item ], file => [ $item ] }; } elsif ( ref $no_index eq 'ARRAY' ) { my $list = $no_index; $no_index = { dir => [ @$list ], file => [ @$list ] }; } # common mistake: files -> file if ( exists $no_index->{files} ) { $no_index->{file} = delete $no_index->{file}; } # common mistake: modules -> module if ( exists $no_index->{modules} ) { $no_index->{module} = delete $no_index->{module}; } return _convert($no_index, $no_index_spec_1_2); } sub _no_index_directory { my ($element, $key, $meta, $version) = @_; return unless $element; # cleanup wrong format if ( ! ref $element ) { my $item = $element; $element = { directory => [ $item ], file => [ $item ] }; } elsif ( ref $element eq 'ARRAY' ) { my $list = $element; $element = { directory => [ @$list ], file => [ @$list ] }; } if ( exists $element->{dir} ) { $element->{directory} = delete $element->{dir}; } # common mistake: files -> file if ( exists $element->{files} ) { $element->{file} = delete $element->{file}; } # common mistake: modules -> module if ( exists $element->{modules} ) { $element->{module} = delete $element->{module}; } my $spec = $version == 2 ? $no_index_spec_2 : $no_index_spec_1_3; return _convert($element, $spec); } sub _is_module_name { my $mod = shift; return unless defined $mod && length $mod; return $mod =~ m{^[A-Za-z][A-Za-z0-9_]*(?:::[A-Za-z0-9_]+)*$}; } sub _clean_version { my ($element) = @_; return 0 if ! defined $element; $element =~ s{^\s*}{}; $element =~ s{\s*$}{}; $element =~ s{^\.}{0.}; return 0 if ! length $element; return 0 if ( $element eq 'undef' || $element eq '' ); my $v = eval { version->new($element) }; # XXX check defined $v and not just $v because version objects leak memory # in boolean context -- dagolden, 2012-02-03 if ( defined $v ) { return _is_qv($v) ? $v->normal : $element; } else { return 0; } } sub _bad_version_hook { my ($v) = @_; $v =~ s{[a-z]+$}{}; # strip trailing alphabetics my $vobj = eval { version->new($v) }; return defined($vobj) ? $vobj : version->new(0); # or give up } sub _version_map { my ($element) = @_; return unless defined $element; if ( ref $element eq 'HASH' ) { # XXX turn this into CPAN::Meta::Requirements with bad version hook # and then turn it back into a hash my $new_map = CPAN::Meta::Requirements->new( { bad_version_hook => \&_bad_version_hook } # punt ); while ( my ($k,$v) = each %$element ) { next unless _is_module_name($k); if ( !defined($v) || !length($v) || $v eq 'undef' || $v eq '' ) { $v = 0; } # some weird, old META have bad yml with module => module # so check if value is like a module name and not like a version if ( _is_module_name($v) && ! version::is_lax($v) ) { $new_map->add_minimum($k => 0); $new_map->add_minimum($v => 0); } $new_map->add_string_requirement($k => $v); } return $new_map->as_string_hash; } elsif ( ref $element eq 'ARRAY' ) { my $hashref = { map { $_ => 0 } @$element }; return _version_map($hashref); # cleanup any weird stuff } elsif ( ref $element eq '' && length $element ) { return { $element => 0 } } return; } sub _prereqs_from_1 { my (undef, undef, $meta) = @_; my $prereqs = {}; for my $phase ( qw/build configure/ ) { my $key = "${phase}_requires"; $prereqs->{$phase}{requires} = _version_map($meta->{$key}) if $meta->{$key}; } for my $rel ( qw/requires recommends conflicts/ ) { $prereqs->{runtime}{$rel} = _version_map($meta->{$rel}) if $meta->{$rel}; } return $prereqs; } my $prereqs_spec = { configure => \&_prereqs_rel, build => \&_prereqs_rel, test => \&_prereqs_rel, runtime => \&_prereqs_rel, develop => \&_prereqs_rel, ':custom' => \&_prefix_custom, }; my $relation_spec = { requires => \&_version_map, recommends => \&_version_map, suggests => \&_version_map, conflicts => \&_version_map, ':custom' => \&_prefix_custom, }; sub _cleanup_prereqs { my ($prereqs, $key, $meta, $to_version) = @_; return unless $prereqs && ref $prereqs eq 'HASH'; return _convert( $prereqs, $prereqs_spec, $to_version ); } sub _prereqs_rel { my ($relation, $key, $meta, $to_version) = @_; return unless $relation && ref $relation eq 'HASH'; return _convert( $relation, $relation_spec, $to_version ); } BEGIN { my @old_prereqs = qw( requires configure_requires recommends conflicts ); for ( @old_prereqs ) { my $sub = "_get_$_"; my ($phase,$type) = split qr/_/, $_; if ( ! defined $type ) { $type = $phase; $phase = 'runtime'; } no strict 'refs'; *{$sub} = sub { _extract_prereqs($_[2]->{prereqs},$phase,$type) }; } } sub _get_build_requires { my ($data, $key, $meta) = @_; my $test_h = _extract_prereqs($_[2]->{prereqs}, qw(test requires)) || {}; my $build_h = _extract_prereqs($_[2]->{prereqs}, qw(build requires)) || {}; my $test_req = CPAN::Meta::Requirements->from_string_hash($test_h); my $build_req = CPAN::Meta::Requirements->from_string_hash($build_h); $test_req->add_requirements($build_req)->as_string_hash; } sub _extract_prereqs { my ($prereqs, $phase, $type) = @_; return unless ref $prereqs eq 'HASH'; return scalar _version_map($prereqs->{$phase}{$type}); } sub _downgrade_optional_features { my (undef, undef, $meta) = @_; return unless exists $meta->{optional_features}; my $origin = $meta->{optional_features}; my $features = {}; for my $name ( keys %$origin ) { $features->{$name} = { description => $origin->{$name}{description}, requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','requires'), configure_requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','configure_requires'), build_requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','build_requires'), recommends => _extract_prereqs($origin->{$name}{prereqs},'runtime','recommends'), conflicts => _extract_prereqs($origin->{$name}{prereqs},'runtime','conflicts'), }; for my $k (keys %{$features->{$name}} ) { delete $features->{$name}{$k} unless defined $features->{$name}{$k}; } } return $features; } sub _upgrade_optional_features { my (undef, undef, $meta) = @_; return unless exists $meta->{optional_features}; my $origin = $meta->{optional_features}; my $features = {}; for my $name ( keys %$origin ) { $features->{$name} = { description => $origin->{$name}{description}, prereqs => _prereqs_from_1(undef, undef, $origin->{$name}), }; delete $features->{$name}{prereqs}{configure}; } return $features; } my $optional_features_2_spec = { description => \&_keep, prereqs => \&_cleanup_prereqs, ':custom' => \&_prefix_custom, }; sub _feature_2 { my ($element, $key, $meta, $to_version) = @_; return unless $element && ref $element eq 'HASH'; _convert( $element, $optional_features_2_spec, $to_version ); } sub _cleanup_optional_features_2 { my ($element, $key, $meta, $to_version) = @_; return unless $element && ref $element eq 'HASH'; my $new_data = {}; for my $k ( keys %$element ) { $new_data->{$k} = _feature_2( $element->{$k}, $k, $meta, $to_version ); } return unless keys %$new_data; return $new_data; } sub _optional_features_1_4 { my ($element) = @_; return unless $element; $element = _optional_features_as_map($element); for my $name ( keys %$element ) { for my $drop ( qw/requires_packages requires_os excluded_os/ ) { delete $element->{$name}{$drop}; } } return $element; } sub _optional_features_as_map { my ($element) = @_; return unless $element; if ( ref $element eq 'ARRAY' ) { my %map; for my $feature ( @$element ) { my (@parts) = %$feature; $map{$parts[0]} = $parts[1]; } $element = \%map; } return $element; } sub _is_urlish { defined $_[0] && $_[0] =~ m{\A[-+.a-z0-9]+:.+}i } sub _url_or_drop { my ($element) = @_; return $element if _is_urlish($element); return; } sub _url_list { my ($element) = @_; return unless $element; $element = _listify( $element ); $element = [ grep { _is_urlish($_) } @$element ]; return unless @$element; return $element; } sub _author_list { my ($element) = @_; return [ 'unknown' ] unless $element; $element = _listify( $element ); $element = [ map { defined $_ && length $_ ? $_ : 'unknown' } @$element ]; return [ 'unknown' ] unless @$element; return $element; } my $resource2_upgrade = { license => sub { return _is_urlish($_[0]) ? _listify( $_[0] ) : undef }, homepage => \&_url_or_drop, bugtracker => sub { my ($item) = @_; return unless $item; if ( $item =~ m{^mailto:(.*)$} ) { return { mailto => $1 } } elsif( _is_urlish($item) ) { return { web => $item } } else { return } }, repository => sub { return _is_urlish($_[0]) ? { url => $_[0] } : undef }, ':custom' => \&_prefix_custom, }; sub _upgrade_resources_2 { my (undef, undef, $meta, $version) = @_; return unless exists $meta->{resources}; return _convert($meta->{resources}, $resource2_upgrade); } my $bugtracker2_spec = { web => \&_url_or_drop, mailto => \&_keep, ':custom' => \&_prefix_custom, }; sub _repo_type { my ($element, $key, $meta, $to_version) = @_; return $element if defined $element; return unless exists $meta->{url}; my $repo_url = $meta->{url}; for my $type ( qw/git svn/ ) { return $type if $repo_url =~ m{\A$type}; } return; } my $repository2_spec = { web => \&_url_or_drop, url => \&_url_or_drop, type => \&_repo_type, ':custom' => \&_prefix_custom, }; my $resources2_cleanup = { license => \&_url_list, homepage => \&_url_or_drop, bugtracker => sub { ref $_[0] ? _convert( $_[0], $bugtracker2_spec ) : undef }, repository => sub { my $data = shift; ref $data ? _convert( $data, $repository2_spec ) : undef }, ':custom' => \&_prefix_custom, }; sub _cleanup_resources_2 { my ($resources, $key, $meta, $to_version) = @_; return unless $resources && ref $resources eq 'HASH'; return _convert($resources, $resources2_cleanup, $to_version); } my $resource1_spec = { license => \&_url_or_drop, homepage => \&_url_or_drop, bugtracker => \&_url_or_drop, repository => \&_url_or_drop, ':custom' => \&_keep, }; sub _resources_1_3 { my (undef, undef, $meta, $version) = @_; return unless exists $meta->{resources}; return _convert($meta->{resources}, $resource1_spec); } *_resources_1_4 = *_resources_1_3; sub _resources_1_2 { my (undef, undef, $meta) = @_; my $resources = $meta->{resources} || {}; if ( $meta->{license_url} && ! $resources->{license} ) { $resources->{license} = $meta->{license_url} if _is_urlish($meta->{license_url}); } return unless keys %$resources; return _convert($resources, $resource1_spec); } my $resource_downgrade_spec = { license => sub { return ref $_[0] ? $_[0]->[0] : $_[0] }, homepage => \&_url_or_drop, bugtracker => sub { return $_[0]->{web} }, repository => sub { return $_[0]->{url} || $_[0]->{web} }, ':custom' => \&_no_prefix_ucfirst_custom, }; sub _downgrade_resources { my (undef, undef, $meta, $version) = @_; return unless exists $meta->{resources}; return _convert($meta->{resources}, $resource_downgrade_spec); } sub _release_status { my ($element, undef, $meta) = @_; return $element if $element && $element =~ m{\A(?:stable|testing|unstable)\z}; return _release_status_from_version(undef, undef, $meta); } sub _release_status_from_version { my (undef, undef, $meta) = @_; my $version = $meta->{version} || ''; return ( $version =~ /_/ ) ? 'testing' : 'stable'; } my $provides_spec = { file => \&_keep, version => \&_keep, }; my $provides_spec_2 = { file => \&_keep, version => \&_keep, ':custom' => \&_prefix_custom, }; sub _provides { my ($element, $key, $meta, $to_version) = @_; return unless defined $element && ref $element eq 'HASH'; my $spec = $to_version == 2 ? $provides_spec_2 : $provides_spec; my $new_data = {}; for my $k ( keys %$element ) { $new_data->{$k} = _convert($element->{$k}, $spec, $to_version); $new_data->{$k}{version} = _clean_version($element->{$k}{version}) if exists $element->{$k}{version}; } return $new_data; } sub _convert { my ($data, $spec, $to_version, $is_fragment) = @_; my $new_data = {}; for my $key ( keys %$spec ) { next if $key eq ':custom' || $key eq ':drop'; next unless my $fcn = $spec->{$key}; if ( $is_fragment && $key eq 'generated_by' ) { $fcn = \&_keep; } die "spec for '$key' is not a coderef" unless ref $fcn && ref $fcn eq 'CODE'; my $new_value = $fcn->($data->{$key}, $key, $data, $to_version); $new_data->{$key} = $new_value if defined $new_value; } my $drop_list = $spec->{':drop'}; my $customizer = $spec->{':custom'} || \&_keep; for my $key ( keys %$data ) { next if $drop_list && grep { $key eq $_ } @$drop_list; next if exists $spec->{$key}; # we handled it $new_data->{ $customizer->($key) } = $data->{$key}; } return $new_data; } #--------------------------------------------------------------------------# # define converters for each conversion #--------------------------------------------------------------------------# # each converts from prior version # special ":custom" field is used for keys not recognized in spec my %up_convert = ( '2-from-1.4' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_2, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # CHANGED TO MANDATORY 'dynamic_config' => \&_keep_or_one, # ADDED MANDATORY 'release_status' => \&_release_status_from_version, # PRIOR OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_upgrade_optional_features, 'provides' => \&_provides, 'resources' => \&_upgrade_resources_2, # ADDED OPTIONAL 'description' => \&_keep, 'prereqs' => \&_prereqs_from_1, # drop these deprecated fields, but only after we convert ':drop' => [ qw( build_requires configure_requires conflicts distribution_type license_url private recommends requires ) ], # other random keys need x_ prefixing ':custom' => \&_prefix_custom, }, '1.4-from-1.3' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_1_4, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_4, # ADDED OPTIONAL 'configure_requires' => \&_keep, # drop these deprecated fields, but only after we convert ':drop' => [ qw( license_url private )], # other random keys are OK if already valid ':custom' => \&_keep }, '1.3-from-1.2' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # drop these deprecated fields, but only after we convert ':drop' => [ qw( license_url private )], # other random keys are OK if already valid ':custom' => \&_keep }, '1.2-from-1.1' => { # PRIOR MANDATORY 'version' => \&_keep, # CHANGED TO MANDATORY 'license' => \&_license_1, 'name' => \&_keep, 'generated_by' => \&_generated_by, # ADDED MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'meta-spec' => \&_change_meta_spec, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_1_2, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'resources' => \&_resources_1_2, # drop these deprecated fields, but only after we convert ':drop' => [ qw( license_url private )], # other random keys are OK if already valid ':custom' => \&_keep }, '1.1-from-1.0' => { # CHANGED TO MANDATORY 'version' => \&_keep, # IMPLIED MANDATORY 'name' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'license_url' => \&_url_or_drop, 'private' => \&_keep, # other random keys are OK if already valid ':custom' => \&_keep }, ); my %down_convert = ( '1.4-from-2' => { # MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_downgrade_license, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # OPTIONAL 'build_requires' => \&_get_build_requires, 'configure_requires' => \&_get_configure_requires, 'conflicts' => \&_get_conflicts, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_downgrade_optional_features, 'provides' => \&_provides, 'recommends' => \&_get_recommends, 'requires' => \&_get_requires, 'resources' => \&_downgrade_resources, # drop these unsupported fields (after conversion) ':drop' => [ qw( description prereqs release_status )], # custom keys will be left unchanged ':custom' => \&_keep }, '1.3-from-1.4' => { # MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # drop these unsupported fields, but only after we convert ':drop' => [ qw( configure_requires )], # other random keys are OK if already valid ':custom' => \&_keep, }, '1.2-from-1.3' => { # MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_1_2, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # other random keys are OK if already valid ':custom' => \&_keep, }, '1.1-from-1.2' => { # MANDATORY 'version' => \&_keep, # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, # OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'private' => \&_keep, 'recommends' => \&_version_map, 'requires' => \&_version_map, # drop unsupported fields ':drop' => [ qw( abstract author provides no_index keywords resources )], # other random keys are OK if already valid ':custom' => \&_keep, }, '1.0-from-1.1' => { # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # other random keys are OK if already valid ':custom' => \&_keep, }, ); my %cleanup = ( '2' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_2, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # CHANGED TO MANDATORY 'dynamic_config' => \&_keep_or_one, # ADDED MANDATORY 'release_status' => \&_release_status, # PRIOR OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_cleanup_optional_features_2, 'provides' => \&_provides, 'resources' => \&_cleanup_resources_2, # ADDED OPTIONAL 'description' => \&_keep, 'prereqs' => \&_cleanup_prereqs, # drop these deprecated fields, but only after we convert ':drop' => [ qw( build_requires configure_requires conflicts distribution_type license_url private recommends requires ) ], # other random keys need x_ prefixing ':custom' => \&_prefix_custom, }, '1.4' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_1_4, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_4, # ADDED OPTIONAL 'configure_requires' => \&_keep, # other random keys are OK if already valid ':custom' => \&_keep }, '1.3' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # other random keys are OK if already valid ':custom' => \&_keep }, '1.2' => { # PRIOR MANDATORY 'version' => \&_keep, # CHANGED TO MANDATORY 'license' => \&_license_1, 'name' => \&_keep, 'generated_by' => \&_generated_by, # ADDED MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'meta-spec' => \&_change_meta_spec, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_1_2, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'resources' => \&_resources_1_2, # other random keys are OK if already valid ':custom' => \&_keep }, '1.1' => { # CHANGED TO MANDATORY 'version' => \&_keep, # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'license_url' => \&_url_or_drop, 'private' => \&_keep, # other random keys are OK if already valid ':custom' => \&_keep }, '1.0' => { # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, 'version' => \&_keep, # IMPLIED OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # other random keys are OK if already valid ':custom' => \&_keep, }, ); # for a given field in a spec version, what fields will it feed # into in the *latest* spec (i.e. v2); meta-spec omitted because # we always expect a meta-spec to be generated my %fragments_generate = ( '2' => { 'abstract' => 'abstract', 'author' => 'author', 'generated_by' => 'generated_by', 'license' => 'license', 'name' => 'name', 'version' => 'version', 'dynamic_config' => 'dynamic_config', 'release_status' => 'release_status', 'keywords' => 'keywords', 'no_index' => 'no_index', 'optional_features' => 'optional_features', 'provides' => 'provides', 'resources' => 'resources', 'description' => 'description', 'prereqs' => 'prereqs', }, '1.4' => { 'abstract' => 'abstract', 'author' => 'author', 'generated_by' => 'generated_by', 'license' => 'license', 'name' => 'name', 'version' => 'version', 'build_requires' => 'prereqs', 'conflicts' => 'prereqs', 'distribution_type' => 'distribution_type', 'dynamic_config' => 'dynamic_config', 'keywords' => 'keywords', 'no_index' => 'no_index', 'optional_features' => 'optional_features', 'provides' => 'provides', 'recommends' => 'prereqs', 'requires' => 'prereqs', 'resources' => 'resources', 'configure_requires' => 'prereqs', }, ); # this is not quite true but will work well enough # as 1.4 is a superset of earlier ones $fragments_generate{$_} = $fragments_generate{'1.4'} for qw/1.3 1.2 1.1 1.0/; #--------------------------------------------------------------------------# # Code #--------------------------------------------------------------------------# #pod =method new #pod #pod my $cmc = CPAN::Meta::Converter->new( $struct ); #pod #pod The constructor should be passed a valid metadata structure but invalid #pod structures are accepted. If no meta-spec version is provided, version 1.0 will #pod be assumed. #pod #pod Optionally, you can provide a C argument after C<$struct>: #pod #pod my $cmc = CPAN::Meta::Converter->new( $struct, default_version => "1.4" ); #pod #pod This is only needed when converting a metadata fragment that does not include a #pod C field. #pod #pod =cut sub new { my ($class,$data,%args) = @_; # create an attributes hash my $self = { 'data' => $data, 'spec' => _extract_spec_version($data, $args{default_version}), }; # create the object return bless $self, $class; } sub _extract_spec_version { my ($data, $default) = @_; my $spec = $data->{'meta-spec'}; # is meta-spec there and valid? return( $default || "1.0" ) unless defined $spec && ref $spec eq 'HASH'; # before meta-spec? # does the version key look like a valid version? my $v = $spec->{version}; if ( defined $v && $v =~ /^\d+(?:\.\d+)?$/ ) { return $v if defined $v && grep { $v eq $_ } keys %known_specs; # known spec return $v+0 if defined $v && grep { $v == $_ } keys %known_specs; # 2.0 => 2 } # otherwise, use heuristics: look for 1.x vs 2.0 fields return "2" if exists $data->{prereqs}; return "1.4" if exists $data->{configure_requires}; return( $default || "1.2" ); # when meta-spec was first defined } #pod =method convert #pod #pod my $new_struct = $cmc->convert( version => "2" ); #pod #pod Returns a new hash reference with the metadata converted to a different form. #pod C will die if any conversion/standardization still results in an #pod invalid structure. #pod #pod Valid parameters include: #pod #pod =over #pod #pod =item * #pod #pod C -- Indicates the desired specification version (e.g. "1.0", "1.1" ... "1.4", "2"). #pod Defaults to the latest version of the CPAN Meta Spec. #pod #pod =back #pod #pod Conversion proceeds through each version in turn. For example, a version 1.2 #pod structure might be converted to 1.3 then 1.4 then finally to version 2. The #pod conversion process attempts to clean-up simple errors and standardize data. #pod For example, if C is given as a scalar, it will converted to an array #pod reference containing the item. (Converting a structure to its own version will #pod also clean-up and standardize.) #pod #pod When data are cleaned and standardized, missing or invalid fields will be #pod replaced with sensible defaults when possible. This may be lossy or imprecise. #pod For example, some badly structured META.yml files on CPAN have prerequisite #pod modules listed as both keys and values: #pod #pod requires => { 'Foo::Bar' => 'Bam::Baz' } #pod #pod These would be split and each converted to a prerequisite with a minimum #pod version of zero. #pod #pod When some mandatory fields are missing or invalid, the conversion will attempt #pod to provide a sensible default or will fill them with a value of 'unknown'. For #pod example a missing or unrecognized C field will result in a C #pod field of 'unknown'. Fields that may get an 'unknown' include: #pod #pod =for :list #pod * abstract #pod * author #pod * license #pod #pod =cut sub convert { my ($self, %args) = @_; my $args = { %args }; my $new_version = $args->{version} || $HIGHEST; my $is_fragment = $args->{is_fragment}; my ($old_version) = $self->{spec}; my $converted = _dclone($self->{data}); if ( $old_version == $new_version ) { $converted = _convert( $converted, $cleanup{$old_version}, $old_version, $is_fragment ); unless ( $args->{is_fragment} ) { my $cmv = CPAN::Meta::Validator->new( $converted ); unless ( $cmv->is_valid ) { my $errs = join("\n", $cmv->errors); die "Failed to clean-up $old_version metadata. Errors:\n$errs\n"; } } return $converted; } elsif ( $old_version > $new_version ) { my @vers = sort { $b <=> $a } keys %known_specs; for my $i ( 0 .. $#vers-1 ) { next if $vers[$i] > $old_version; last if $vers[$i+1] < $new_version; my $spec_string = "$vers[$i+1]-from-$vers[$i]"; $converted = _convert( $converted, $down_convert{$spec_string}, $vers[$i+1], $is_fragment ); unless ( $args->{is_fragment} ) { my $cmv = CPAN::Meta::Validator->new( $converted ); unless ( $cmv->is_valid ) { my $errs = join("\n", $cmv->errors); die "Failed to downconvert metadata to $vers[$i+1]. Errors:\n$errs\n"; } } } return $converted; } else { my @vers = sort { $a <=> $b } keys %known_specs; for my $i ( 0 .. $#vers-1 ) { next if $vers[$i] < $old_version; last if $vers[$i+1] > $new_version; my $spec_string = "$vers[$i+1]-from-$vers[$i]"; $converted = _convert( $converted, $up_convert{$spec_string}, $vers[$i+1], $is_fragment ); unless ( $args->{is_fragment} ) { my $cmv = CPAN::Meta::Validator->new( $converted ); unless ( $cmv->is_valid ) { my $errs = join("\n", $cmv->errors); die "Failed to upconvert metadata to $vers[$i+1]. Errors:\n$errs\n"; } } } return $converted; } } #pod =method upgrade_fragment #pod #pod my $new_struct = $cmc->upgrade_fragment; #pod #pod Returns a new hash reference with the metadata converted to the latest version #pod of the CPAN Meta Spec. No validation is done on the result -- you must #pod validate after merging fragments into a complete metadata document. #pod #pod =cut sub upgrade_fragment { my ($self) = @_; my ($old_version) = $self->{spec}; my %expected = map {; $_ => 1 } grep { defined } map { $fragments_generate{$old_version}{$_} } keys %{ $self->{data} }; my $converted = $self->convert( version => $HIGHEST, is_fragment => 1 ); for my $key ( keys %$converted ) { next if $key =~ /^x_/i || $key eq 'meta-spec'; delete $converted->{$key} unless $expected{$key}; } return $converted; } 1; # ABSTRACT: Convert CPAN distribution metadata structures =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Converter - Convert CPAN distribution metadata structures =head1 VERSION version 2.143240 =head1 SYNOPSIS my $struct = decode_json_file('META.json'); my $cmc = CPAN::Meta::Converter->new( $struct ); my $new_struct = $cmc->convert( version => "2" ); =head1 DESCRIPTION This module converts CPAN Meta structures from one form to another. The primary use is to convert older structures to the most modern version of the specification, but other transformations may be implemented in the future as needed. (E.g. stripping all custom fields or stripping all optional fields.) =head1 METHODS =head2 new my $cmc = CPAN::Meta::Converter->new( $struct ); The constructor should be passed a valid metadata structure but invalid structures are accepted. If no meta-spec version is provided, version 1.0 will be assumed. Optionally, you can provide a C argument after C<$struct>: my $cmc = CPAN::Meta::Converter->new( $struct, default_version => "1.4" ); This is only needed when converting a metadata fragment that does not include a C field. =head2 convert my $new_struct = $cmc->convert( version => "2" ); Returns a new hash reference with the metadata converted to a different form. C will die if any conversion/standardization still results in an invalid structure. Valid parameters include: =over =item * C -- Indicates the desired specification version (e.g. "1.0", "1.1" ... "1.4", "2"). Defaults to the latest version of the CPAN Meta Spec. =back Conversion proceeds through each version in turn. For example, a version 1.2 structure might be converted to 1.3 then 1.4 then finally to version 2. The conversion process attempts to clean-up simple errors and standardize data. For example, if C is given as a scalar, it will converted to an array reference containing the item. (Converting a structure to its own version will also clean-up and standardize.) When data are cleaned and standardized, missing or invalid fields will be replaced with sensible defaults when possible. This may be lossy or imprecise. For example, some badly structured META.yml files on CPAN have prerequisite modules listed as both keys and values: requires => { 'Foo::Bar' => 'Bam::Baz' } These would be split and each converted to a prerequisite with a minimum version of zero. When some mandatory fields are missing or invalid, the conversion will attempt to provide a sensible default or will fill them with a value of 'unknown'. For example a missing or unrecognized C field will result in a C field of 'unknown'. Fields that may get an 'unknown' include: =over 4 =item * abstract =item * author =item * license =back =head2 upgrade_fragment my $new_struct = $cmc->upgrade_fragment; Returns a new hash reference with the metadata converted to the latest version of the CPAN Meta Spec. No validation is done on the result -- you must validate after merging fragments into a complete metadata document. =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__ # vim: ts=2 sts=2 sw=2 et: CPAN_META_CONVERTER $fatpacked{"CPAN/Meta/Feature.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_FEATURE'; use 5.006; use strict; use warnings; package CPAN::Meta::Feature; # VERSION $CPAN::Meta::Feature::VERSION = '2.143240'; use CPAN::Meta::Prereqs; #pod =head1 DESCRIPTION #pod #pod A CPAN::Meta::Feature object describes an optional feature offered by a CPAN #pod distribution and specified in the distribution's F (or F) #pod file. #pod #pod For the most part, this class will only be used when operating on the result of #pod the C or C methods on a L object. #pod #pod =method new #pod #pod my $feature = CPAN::Meta::Feature->new( $identifier => \%spec ); #pod #pod This returns a new Feature object. The C<%spec> argument to the constructor #pod should be the same as the value of the C entry in the #pod distmeta. It must contain entries for C and C. #pod #pod =cut sub new { my ($class, $identifier, $spec) = @_; my %guts = ( identifier => $identifier, description => $spec->{description}, prereqs => CPAN::Meta::Prereqs->new($spec->{prereqs}), ); bless \%guts => $class; } #pod =method identifier #pod #pod This method returns the feature's identifier. #pod #pod =cut sub identifier { $_[0]{identifier} } #pod =method description #pod #pod This method returns the feature's long description. #pod #pod =cut sub description { $_[0]{description} } #pod =method prereqs #pod #pod This method returns the feature's prerequisites as a L #pod object. #pod #pod =cut sub prereqs { $_[0]{prereqs} } 1; # ABSTRACT: an optional feature provided by a CPAN distribution __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Feature - an optional feature provided by a CPAN distribution =head1 VERSION version 2.143240 =head1 DESCRIPTION A CPAN::Meta::Feature object describes an optional feature offered by a CPAN distribution and specified in the distribution's F (or F) file. For the most part, this class will only be used when operating on the result of the C or C methods on a L object. =head1 METHODS =head2 new my $feature = CPAN::Meta::Feature->new( $identifier => \%spec ); This returns a new Feature object. The C<%spec> argument to the constructor should be the same as the value of the C entry in the distmeta. It must contain entries for C and C. =head2 identifier This method returns the feature's identifier. =head2 description This method returns the feature's long description. =head2 prereqs This method returns the feature's prerequisites as a L object. =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_FEATURE $fatpacked{"CPAN/Meta/History.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_HISTORY'; # vi:tw=72 use 5.006; use strict; use warnings; package CPAN::Meta::History; # VERSION $CPAN::Meta::History::VERSION = '2.143240'; 1; # ABSTRACT: history of CPAN Meta Spec changes __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::History - history of CPAN Meta Spec changes =head1 VERSION version 2.143240 =head1 DESCRIPTION The CPAN Meta Spec has gone through several iterations. It was originally written in HTML and later revised into POD (though published in HTML generated from the POD). Fields were added, removed or changed, sometimes by design and sometimes to reflect real-world usage after the fact. This document reconstructs the history of the CPAN Meta Spec based on change logs, repository commit messages and the published HTML files. In some cases, particularly prior to version 1.2, the exact version when certain fields were introduced or changed is inconsistent between sources. When in doubt, the published HTML files for versions 1.0 to 1.4 as they existed when version 2 was developed are used as the definitive source. Starting with version 2, the specification document is part of the CPAN-Meta distribution and will be published on CPAN as L. Going forward, specification version numbers will be integers and decimal portions will correspond to a release date for the CPAN::Meta library. =head1 HISTORY =head2 Version 2 April 2010 =over =item * Revised spec examples as perl data structures rather than YAML =item * Switched to JSON serialization from YAML =item * Specified allowed version number formats =item * Replaced 'requires', 'build_requires', 'configure_requires', 'recommends' and 'conflicts' with new 'prereqs' data structure divided by I (configure, build, test, runtime, etc.) and I (requires, recommends, suggests, conflicts) =item * Added support for 'develop' phase for requirements for maintaining a list of authoring tools =item * Changed 'license' to a list and revised the set of valid licenses =item * Made 'dynamic_config' mandatory to reduce confusion =item * Changed 'resources' subkey 'repository' to a hash that clarifies repository type, url for browsing and url for checkout =item * Changed 'resources' subkey 'bugtracker' to a hash for either web or mailto resource =item * Changed specification of 'optional_features': =over =item * Added formal specification and usage guide instead of just example =item * Changed to use new prereqs data structure instead of individual keys =back =item * Clarified intended use of 'author' as generalized contact list =item * Added 'release_status' field to indicate stable, testing or unstable status to provide hints to indexers =item * Added 'description' field for a longer description of the distribution =item * Formalized use of "x_" or "X_" for all custom keys not listed in the official spec =back =head2 Version 1.4 June 2008 =over =item * Noted explicit support for 'perl' in prerequisites =item * Added 'configure_requires' prerequisite type =item * Changed 'optional_features' =over =item * Example corrected to show map of maps instead of list of maps (though descriptive text said 'map' even in v1.3) =item * Removed 'requires_packages', 'requires_os' and 'excluded_os' as valid subkeys =back =back =head2 Version 1.3 November 2006 =over =item * Added 'no_index' subkey 'directory' and removed 'dir' to match actual usage in the wild =item * Added a 'repository' subkey to 'resources' =back =head2 Version 1.2 August 2005 =over =item * Re-wrote and restructured spec in POD syntax =item * Changed 'name' to be mandatory =item * Changed 'generated_by' to be mandatory =item * Changed 'license' to be mandatory =item * Added version range specifications for prerequisites =item * Added required 'abstract' field =item * Added required 'author' field =item * Added required 'meta-spec' field to define 'version' (and 'url') of the CPAN Meta Spec used for metadata =item * Added 'provides' field =item * Added 'no_index' field and deprecated 'private' field. 'no_index' subkeys include 'file', 'dir', 'package' and 'namespace' =item * Added 'keywords' field =item * Added 'resources' field with subkeys 'homepage', 'license', and 'bugtracker' =item * Added 'optional_features' field as an alternate under 'recommends'. Includes 'description', 'requires', 'build_requires', 'conflicts', 'requires_packages', 'requires_os' and 'excluded_os' as valid subkeys =item * Removed 'license_uri' field =back =head2 Version 1.1 May 2003 =over =item * Changed 'version' to be mandatory =item * Added 'private' field =item * Added 'license_uri' field =back =head2 Version 1.0 March 2003 =over =item * Original release (in HTML format only) =item * Included 'name', 'version', 'license', 'distribution_type', 'requires', 'recommends', 'build_requires', 'conflicts', 'dynamic_config', 'generated_by' =back =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_HISTORY $fatpacked{"CPAN/Meta/Merge.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_MERGE'; use strict; use warnings; package CPAN::Meta::Merge; # VERSION $CPAN::Meta::Merge::VERSION = '2.143240'; use Carp qw/croak/; use Scalar::Util qw/blessed/; use CPAN::Meta::Converter; sub _identical { my ($left, $right, $path) = @_; croak sprintf "Can't merge attribute %s: '%s' does not equal '%s'", join('.', @{$path}), $left, $right unless $left eq $right; return $left; } sub _merge { my ($current, $next, $mergers, $path) = @_; for my $key (keys %{$next}) { if (not exists $current->{$key}) { $current->{$key} = $next->{$key}; } elsif (my $merger = $mergers->{$key}) { $current->{$key} = $merger->($current->{$key}, $next->{$key}, [ @{$path}, $key ]); } elsif ($merger = $mergers->{':default'}) { $current->{$key} = $merger->($current->{$key}, $next->{$key}, [ @{$path}, $key ]); } else { croak sprintf "Can't merge unknown attribute '%s'", join '.', @{$path}, $key; } } return $current; } sub _uniq { my %seen = (); return grep { not $seen{$_}++ } @_; } sub _set_addition { my ($left, $right) = @_; return [ +_uniq(@{$left}, @{$right}) ]; } sub _uniq_map { my ($left, $right, $path) = @_; for my $key (keys %{$right}) { if (not exists $left->{$key}) { $left->{$key} = $right->{$key}; } else { croak 'Duplication of element ' . join '.', @{$path}, $key; } } return $left; } sub _improvize { my ($left, $right, $path) = @_; my ($name) = reverse @{$path}; if ($name =~ /^x_/) { if (ref($left) eq 'ARRAY') { return _set_addition($left, $right, $path); } elsif (ref($left) eq 'HASH') { return _uniq_map($left, $right, $path); } else { return _identical($left, $right, $path); } } croak sprintf "Can't merge '%s'", join '.', @{$path}; } sub _optional_features { my ($left, $right, $path) = @_; for my $key (keys %{$right}) { if (not exists $left->{$key}) { $left->{$key} = $right->{$key}; } else { for my $subkey (keys %{ $right->{$key} }) { next if $subkey eq 'prereqs'; if (not exists $left->{$key}{$subkey}) { $left->{$key}{$subkey} = $right->{$key}{$subkey}; } else { Carp::croak "Cannot merge two optional_features named '$key' with different '$subkey' values" if do { no warnings 'uninitialized'; $left->{$key}{$subkey} ne $right->{$key}{$subkey} }; } } require CPAN::Meta::Prereqs; $left->{$key}{prereqs} = CPAN::Meta::Prereqs->new($left->{$key}{prereqs}) ->with_merged_prereqs(CPAN::Meta::Prereqs->new($right->{$key}{prereqs})) ->as_string_hash; } } return $left; } my %default = ( abstract => \&_identical, author => \&_set_addition, dynamic_config => sub { my ($left, $right) = @_; return $left || $right; }, generated_by => sub { my ($left, $right) = @_; return join ', ', _uniq(split(/, /, $left), split(/, /, $right)); }, license => \&_set_addition, 'meta-spec' => { version => \&_identical, url => \&_identical }, name => \&_identical, release_status => \&_identical, version => \&_identical, description => \&_identical, keywords => \&_set_addition, no_index => { map { ($_ => \&_set_addition) } qw/file directory package namespace/ }, optional_features => \&_optional_features, prereqs => sub { require CPAN::Meta::Prereqs; my ($left, $right) = map { CPAN::Meta::Prereqs->new($_) } @_[0,1]; return $left->with_merged_prereqs($right)->as_string_hash; }, provides => \&_uniq_map, resources => { license => \&_set_addition, homepage => \&_identical, bugtracker => \&_uniq_map, repository => \&_uniq_map, ':default' => \&_improvize, }, ':default' => \&_improvize, ); sub new { my ($class, %arguments) = @_; croak 'default version required' if not exists $arguments{default_version}; my %mapping = %default; my %extra = %{ $arguments{extra_mappings} || {} }; for my $key (keys %extra) { if (ref($mapping{$key}) eq 'HASH') { $mapping{$key} = { %{ $mapping{$key} }, %{ $extra{$key} } }; } else { $mapping{$key} = $extra{$key}; } } return bless { default_version => $arguments{default_version}, mapping => _coerce_mapping(\%mapping, []), }, $class; } my %coderef_for = ( set_addition => \&_set_addition, uniq_map => \&_uniq_map, identical => \&_identical, improvize => \&_improvize, ); sub _coerce_mapping { my ($orig, $map_path) = @_; my %ret; for my $key (keys %{$orig}) { my $value = $orig->{$key}; if (ref($orig->{$key}) eq 'CODE') { $ret{$key} = $value; } elsif (ref($value) eq 'HASH') { my $mapping = _coerce_mapping($value, [ @{$map_path}, $key ]); $ret{$key} = sub { my ($left, $right, $path) = @_; return _merge($left, $right, $mapping, [ @{$path} ]); }; } elsif ($coderef_for{$value}) { $ret{$key} = $coderef_for{$value}; } else { croak "Don't know what to do with " . join '.', @{$map_path}, $key; } } return \%ret; } sub merge { my ($self, @items) = @_; my $current = {}; for my $next (@items) { if ( blessed($next) && $next->isa('CPAN::Meta') ) { $next = $next->as_struct; } elsif ( ref($next) eq 'HASH' ) { my $cmc = CPAN::Meta::Converter->new( $next, default_version => $self->{default_version} ); $next = $cmc->upgrade_fragment; } else { croak "Don't know how to merge '$next'"; } $current = _merge($current, $next, $self->{mapping}, []); } return $current; } 1; # ABSTRACT: Merging CPAN Meta fragments __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Merge - Merging CPAN Meta fragments =head1 VERSION version 2.143240 =head1 SYNOPSIS my $merger = CPAN::Meta::Merge->new(default_version => "2"); my $meta = $merger->merge($base, @additional); =head1 DESCRIPTION =head1 METHODS =head2 new This creates a CPAN::Meta::Merge object. It takes one mandatory named argument, C, declaring the version of the meta-spec that must be used for the merge. It can optionally take an C argument that allows one to add additional merging functions for specific elements. =head2 merge(@fragments) Merge all C<@fragments> together. It will accept both CPAN::Meta objects and (possibly incomplete) hashrefs of metadata. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_MERGE $fatpacked{"CPAN/Meta/Prereqs.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_PREREQS'; use 5.006; use strict; use warnings; package CPAN::Meta::Prereqs; # VERSION $CPAN::Meta::Prereqs::VERSION = '2.143240'; #pod =head1 DESCRIPTION #pod #pod A CPAN::Meta::Prereqs object represents the prerequisites for a CPAN #pod distribution or one of its optional features. Each set of prereqs is #pod organized by phase and type, as described in L. #pod #pod =cut use Carp qw(confess); use Scalar::Util qw(blessed); use CPAN::Meta::Requirements 2.121; #pod =method new #pod #pod my $prereq = CPAN::Meta::Prereqs->new( \%prereq_spec ); #pod #pod This method returns a new set of Prereqs. The input should look like the #pod contents of the C field described in L, meaning #pod something more or less like this: #pod #pod my $prereq = CPAN::Meta::Prereqs->new({ #pod runtime => { #pod requires => { #pod 'Some::Module' => '1.234', #pod ..., #pod }, #pod ..., #pod }, #pod ..., #pod }); #pod #pod You can also construct an empty set of prereqs with: #pod #pod my $prereqs = CPAN::Meta::Prereqs->new; #pod #pod This empty set of prereqs is useful for accumulating new prereqs before finally #pod dumping the whole set into a structure or string. #pod #pod =cut sub __legal_phases { qw(configure build test runtime develop) } sub __legal_types { qw(requires recommends suggests conflicts) } # expect a prereq spec from META.json -- rjbs, 2010-04-11 sub new { my ($class, $prereq_spec) = @_; $prereq_spec ||= {}; my %is_legal_phase = map {; $_ => 1 } $class->__legal_phases; my %is_legal_type = map {; $_ => 1 } $class->__legal_types; my %guts; PHASE: for my $phase (keys %$prereq_spec) { next PHASE unless $phase =~ /\Ax_/i or $is_legal_phase{$phase}; my $phase_spec = $prereq_spec->{ $phase }; next PHASE unless keys %$phase_spec; TYPE: for my $type (keys %$phase_spec) { next TYPE unless $type =~ /\Ax_/i or $is_legal_type{$type}; my $spec = $phase_spec->{ $type }; next TYPE unless keys %$spec; $guts{prereqs}{$phase}{$type} = CPAN::Meta::Requirements->from_string_hash( $spec ); } } return bless \%guts => $class; } #pod =method requirements_for #pod #pod my $requirements = $prereqs->requirements_for( $phase, $type ); #pod #pod This method returns a L object for the given #pod phase/type combination. If no prerequisites are registered for that #pod combination, a new CPAN::Meta::Requirements object will be returned, and it may #pod be added to as needed. #pod #pod If C<$phase> or C<$type> are undefined or otherwise invalid, an exception will #pod be raised. #pod #pod =cut sub requirements_for { my ($self, $phase, $type) = @_; confess "requirements_for called without phase" unless defined $phase; confess "requirements_for called without type" unless defined $type; unless ($phase =~ /\Ax_/i or grep { $phase eq $_ } $self->__legal_phases) { confess "requested requirements for unknown phase: $phase"; } unless ($type =~ /\Ax_/i or grep { $type eq $_ } $self->__legal_types) { confess "requested requirements for unknown type: $type"; } my $req = ($self->{prereqs}{$phase}{$type} ||= CPAN::Meta::Requirements->new); $req->finalize if $self->is_finalized; return $req; } #pod =method with_merged_prereqs #pod #pod my $new_prereqs = $prereqs->with_merged_prereqs( $other_prereqs ); #pod #pod my $new_prereqs = $prereqs->with_merged_prereqs( \@other_prereqs ); #pod #pod This method returns a new CPAN::Meta::Prereqs objects in which all the #pod other prerequisites given are merged into the current set. This is primarily #pod provided for combining a distribution's core prereqs with the prereqs of one of #pod its optional features. #pod #pod The new prereqs object has no ties to the originals, and altering it further #pod will not alter them. #pod #pod =cut sub with_merged_prereqs { my ($self, $other) = @_; my @other = blessed($other) ? $other : @$other; my @prereq_objs = ($self, @other); my %new_arg; for my $phase ($self->__legal_phases) { for my $type ($self->__legal_types) { my $req = CPAN::Meta::Requirements->new; for my $prereq (@prereq_objs) { my $this_req = $prereq->requirements_for($phase, $type); next unless $this_req->required_modules; $req->add_requirements($this_req); } next unless $req->required_modules; $new_arg{ $phase }{ $type } = $req->as_string_hash; } } return (ref $self)->new(\%new_arg); } #pod =method merged_requirements #pod #pod my $new_reqs = $prereqs->merged_requirements( \@phases, \@types ); #pod my $new_reqs = $prereqs->merged_requirements( \@phases ); #pod my $new_reqs = $preerqs->merged_requirements(); #pod #pod This method joins together all requirements across a number of phases #pod and types into a new L object. If arguments #pod are omitted, it defaults to "runtime", "build" and "test" for phases #pod and "requires" and "recommends" for types. #pod #pod =cut sub merged_requirements { my ($self, $phases, $types) = @_; $phases = [qw/runtime build test/] unless defined $phases; $types = [qw/requires recommends/] unless defined $types; confess "merged_requirements phases argument must be an arrayref" unless ref $phases eq 'ARRAY'; confess "merged_requirements types argument must be an arrayref" unless ref $types eq 'ARRAY'; my $req = CPAN::Meta::Requirements->new; for my $phase ( @$phases ) { unless ($phase =~ /\Ax_/i or grep { $phase eq $_ } $self->__legal_phases) { confess "requested requirements for unknown phase: $phase"; } for my $type ( @$types ) { unless ($type =~ /\Ax_/i or grep { $type eq $_ } $self->__legal_types) { confess "requested requirements for unknown type: $type"; } $req->add_requirements( $self->requirements_for($phase, $type) ); } } $req->finalize if $self->is_finalized; return $req; } #pod =method as_string_hash #pod #pod This method returns a hashref containing structures suitable for dumping into a #pod distmeta data structure. It is made up of hashes and strings, only; there will #pod be no Prereqs, CPAN::Meta::Requirements, or C objects inside it. #pod #pod =cut sub as_string_hash { my ($self) = @_; my %hash; for my $phase ($self->__legal_phases) { for my $type ($self->__legal_types) { my $req = $self->requirements_for($phase, $type); next unless $req->required_modules; $hash{ $phase }{ $type } = $req->as_string_hash; } } return \%hash; } #pod =method is_finalized #pod #pod This method returns true if the set of prereqs has been marked "finalized," and #pod cannot be altered. #pod #pod =cut sub is_finalized { $_[0]{finalized} } #pod =method finalize #pod #pod Calling C on a Prereqs object will close it for further modification. #pod Attempting to make any changes that would actually alter the prereqs will #pod result in an exception being thrown. #pod #pod =cut sub finalize { my ($self) = @_; $self->{finalized} = 1; for my $phase (keys %{ $self->{prereqs} }) { $_->finalize for values %{ $self->{prereqs}{$phase} }; } } #pod =method clone #pod #pod my $cloned_prereqs = $prereqs->clone; #pod #pod This method returns a Prereqs object that is identical to the original object, #pod but can be altered without affecting the original object. Finalization does #pod not survive cloning, meaning that you may clone a finalized set of prereqs and #pod then modify the clone. #pod #pod =cut sub clone { my ($self) = @_; my $clone = (ref $self)->new( $self->as_string_hash ); } 1; # ABSTRACT: a set of distribution prerequisites by phase and type __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Prereqs - a set of distribution prerequisites by phase and type =head1 VERSION version 2.143240 =head1 DESCRIPTION A CPAN::Meta::Prereqs object represents the prerequisites for a CPAN distribution or one of its optional features. Each set of prereqs is organized by phase and type, as described in L. =head1 METHODS =head2 new my $prereq = CPAN::Meta::Prereqs->new( \%prereq_spec ); This method returns a new set of Prereqs. The input should look like the contents of the C field described in L, meaning something more or less like this: my $prereq = CPAN::Meta::Prereqs->new({ runtime => { requires => { 'Some::Module' => '1.234', ..., }, ..., }, ..., }); You can also construct an empty set of prereqs with: my $prereqs = CPAN::Meta::Prereqs->new; This empty set of prereqs is useful for accumulating new prereqs before finally dumping the whole set into a structure or string. =head2 requirements_for my $requirements = $prereqs->requirements_for( $phase, $type ); This method returns a L object for the given phase/type combination. If no prerequisites are registered for that combination, a new CPAN::Meta::Requirements object will be returned, and it may be added to as needed. If C<$phase> or C<$type> are undefined or otherwise invalid, an exception will be raised. =head2 with_merged_prereqs my $new_prereqs = $prereqs->with_merged_prereqs( $other_prereqs ); my $new_prereqs = $prereqs->with_merged_prereqs( \@other_prereqs ); This method returns a new CPAN::Meta::Prereqs objects in which all the other prerequisites given are merged into the current set. This is primarily provided for combining a distribution's core prereqs with the prereqs of one of its optional features. The new prereqs object has no ties to the originals, and altering it further will not alter them. =head2 merged_requirements my $new_reqs = $prereqs->merged_requirements( \@phases, \@types ); my $new_reqs = $prereqs->merged_requirements( \@phases ); my $new_reqs = $preerqs->merged_requirements(); This method joins together all requirements across a number of phases and types into a new L object. If arguments are omitted, it defaults to "runtime", "build" and "test" for phases and "requires" and "recommends" for types. =head2 as_string_hash This method returns a hashref containing structures suitable for dumping into a distmeta data structure. It is made up of hashes and strings, only; there will be no Prereqs, CPAN::Meta::Requirements, or C objects inside it. =head2 is_finalized This method returns true if the set of prereqs has been marked "finalized," and cannot be altered. =head2 finalize Calling C on a Prereqs object will close it for further modification. Attempting to make any changes that would actually alter the prereqs will result in an exception being thrown. =head2 clone my $cloned_prereqs = $prereqs->clone; This method returns a Prereqs object that is identical to the original object, but can be altered without affecting the original object. Finalization does not survive cloning, meaning that you may clone a finalized set of prereqs and then modify the clone. =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_PREREQS $fatpacked{"CPAN/Meta/Requirements.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_REQUIREMENTS'; use strict; use warnings; package CPAN::Meta::Requirements; # ABSTRACT: a set of version requirements for a CPAN dist our $VERSION = '2.131'; #pod =head1 SYNOPSIS #pod #pod use CPAN::Meta::Requirements; #pod #pod my $build_requires = CPAN::Meta::Requirements->new; #pod #pod $build_requires->add_minimum('Library::Foo' => 1.208); #pod #pod $build_requires->add_minimum('Library::Foo' => 2.602); #pod #pod $build_requires->add_minimum('Module::Bar' => 'v1.2.3'); #pod #pod $METAyml->{build_requires} = $build_requires->as_string_hash; #pod #pod =head1 DESCRIPTION #pod #pod A CPAN::Meta::Requirements object models a set of version constraints like #pod those specified in the F or F files in CPAN distributions, #pod and as defined by L; #pod It can be built up by adding more and more constraints, and it will reduce them #pod to the simplest representation. #pod #pod Logically impossible constraints will be identified immediately by thrown #pod exceptions. #pod #pod =cut use Carp (); # To help ExtUtils::MakeMaker bootstrap CPAN::Meta::Requirements on perls # before 5.10, we fall back to the EUMM bundled compatibility version module if # that's the only thing available. This shouldn't ever happen in a normal CPAN # install of CPAN::Meta::Requirements, as version.pm will be picked up from # prereqs and be available at runtime. BEGIN { eval "use version ()"; ## no critic if ( my $err = $@ ) { eval "require ExtUtils::MakeMaker::version" or die $err; ## no critic } } # Perl 5.10.0 didn't have "is_qv" in version.pm *_is_qv = version->can('is_qv') ? sub { $_[0]->is_qv } : sub { exists $_[0]->{qv} }; # construct once, reuse many times my $V0 = version->new(0); #pod =method new #pod #pod my $req = CPAN::Meta::Requirements->new; #pod #pod This returns a new CPAN::Meta::Requirements object. It takes an optional #pod hash reference argument. Currently, only one key is supported: #pod #pod =for :list #pod * C -- if provided, when a version cannot be parsed into #pod a version object, this code reference will be called with the invalid #pod version string as first argument, and the module name as second #pod argument. It must return a valid version object. #pod #pod All other keys are ignored. #pod #pod =cut my @valid_options = qw( bad_version_hook ); sub new { my ($class, $options) = @_; $options ||= {}; Carp::croak "Argument to $class\->new() must be a hash reference" unless ref $options eq 'HASH'; my %self = map {; $_ => $options->{$_}} @valid_options; return bless \%self => $class; } # from version::vpp sub _find_magic_vstring { my $value = shift; my $tvalue = ''; require B; my $sv = B::svref_2object(\$value); my $magic = ref($sv) eq 'B::PVMG' ? $sv->MAGIC : undef; while ( $magic ) { if ( $magic->TYPE eq 'V' ) { $tvalue = $magic->PTR; $tvalue =~ s/^v?(.+)$/v$1/; last; } else { $magic = $magic->MOREMAGIC; } } return $tvalue; } # safe if given an unblessed reference sub _isa_version { UNIVERSAL::isa( $_[0], 'UNIVERSAL' ) && $_[0]->isa('version') } sub _version_object { my ($self, $module, $version) = @_; my $vobj; # hack around version::vpp not handling <3 character vstring literals if ( $INC{'version/vpp.pm'} || $INC{'ExtUtils/MakeMaker/version/vpp.pm'} ) { my $magic = _find_magic_vstring( $version ); $version = $magic if length $magic; } eval { if (not defined $version or $version eq '0') { $vobj = $V0; } elsif ( ref($version) eq 'version' || _isa_version($version) ) { $vobj = $version; } else { local $SIG{__WARN__} = sub { die "Invalid version: $_[0]" }; $vobj = version->new($version); } }; if ( my $err = $@ ) { my $hook = $self->{bad_version_hook}; $vobj = eval { $hook->($version, $module) } if ref $hook eq 'CODE'; unless (eval { $vobj->isa("version") }) { $err =~ s{ at .* line \d+.*$}{}; die "Can't convert '$version': $err"; } } # ensure no leading '.' if ( $vobj =~ m{\A\.} ) { $vobj = version->new("0$vobj"); } # ensure normal v-string form if ( _is_qv($vobj) ) { $vobj = version->new($vobj->normal); } return $vobj; } #pod =method add_minimum #pod #pod $req->add_minimum( $module => $version ); #pod #pod This adds a new minimum version requirement. If the new requirement is #pod redundant to the existing specification, this has no effect. #pod #pod Minimum requirements are inclusive. C<$version> is required, along with any #pod greater version number. #pod #pod This method returns the requirements object. #pod #pod =method add_maximum #pod #pod $req->add_maximum( $module => $version ); #pod #pod This adds a new maximum version requirement. If the new requirement is #pod redundant to the existing specification, this has no effect. #pod #pod Maximum requirements are inclusive. No version strictly greater than the given #pod version is allowed. #pod #pod This method returns the requirements object. #pod #pod =method add_exclusion #pod #pod $req->add_exclusion( $module => $version ); #pod #pod This adds a new excluded version. For example, you might use these three #pod method calls: #pod #pod $req->add_minimum( $module => '1.00' ); #pod $req->add_maximum( $module => '1.82' ); #pod #pod $req->add_exclusion( $module => '1.75' ); #pod #pod Any version between 1.00 and 1.82 inclusive would be acceptable, except for #pod 1.75. #pod #pod This method returns the requirements object. #pod #pod =method exact_version #pod #pod $req->exact_version( $module => $version ); #pod #pod This sets the version required for the given module to I the given #pod version. No other version would be considered acceptable. #pod #pod This method returns the requirements object. #pod #pod =cut BEGIN { for my $type (qw(maximum exclusion exact_version)) { my $method = "with_$type"; my $to_add = $type eq 'exact_version' ? $type : "add_$type"; my $code = sub { my ($self, $name, $version) = @_; $version = $self->_version_object( $name, $version ); $self->__modify_entry_for($name, $method, $version); return $self; }; no strict 'refs'; *$to_add = $code; } } sub add_minimum { my ($self, $name, $version) = @_; if (not defined $version or $version eq '0') { return $self if $self->__entry_for($name); Carp::confess("can't add new requirements to finalized requirements") if $self->is_finalized; $self->{requirements}{ $name } = CPAN::Meta::Requirements::_Range::Range->with_minimum($V0); } else { $version = $self->_version_object( $name, $version ); $self->__modify_entry_for($name, 'with_minimum', $version); } return $self; } #pod =method add_requirements #pod #pod $req->add_requirements( $another_req_object ); #pod #pod This method adds all the requirements in the given CPAN::Meta::Requirements object #pod to the requirements object on which it was called. If there are any conflicts, #pod an exception is thrown. #pod #pod This method returns the requirements object. #pod #pod =cut sub add_requirements { my ($self, $req) = @_; for my $module ($req->required_modules) { my $modifiers = $req->__entry_for($module)->as_modifiers; for my $modifier (@$modifiers) { my ($method, @args) = @$modifier; $self->$method($module => @args); }; } return $self; } #pod =method accepts_module #pod #pod my $bool = $req->accepts_module($module => $version); #pod #pod Given an module and version, this method returns true if the version #pod specification for the module accepts the provided version. In other words, #pod given: #pod #pod Module => '>= 1.00, < 2.00' #pod #pod We will accept 1.00 and 1.75 but not 0.50 or 2.00. #pod #pod For modules that do not appear in the requirements, this method will return #pod true. #pod #pod =cut sub accepts_module { my ($self, $module, $version) = @_; $version = $self->_version_object( $module, $version ); return 1 unless my $range = $self->__entry_for($module); return $range->_accepts($version); } #pod =method clear_requirement #pod #pod $req->clear_requirement( $module ); #pod #pod This removes the requirement for a given module from the object. #pod #pod This method returns the requirements object. #pod #pod =cut sub clear_requirement { my ($self, $module) = @_; return $self unless $self->__entry_for($module); Carp::confess("can't clear requirements on finalized requirements") if $self->is_finalized; delete $self->{requirements}{ $module }; return $self; } #pod =method requirements_for_module #pod #pod $req->requirements_for_module( $module ); #pod #pod This returns a string containing the version requirements for a given module in #pod the format described in L or undef if the given module has no #pod requirements. This should only be used for informational purposes such as error #pod messages and should not be interpreted or used for comparison (see #pod L instead.) #pod #pod =cut sub requirements_for_module { my ($self, $module) = @_; my $entry = $self->__entry_for($module); return unless $entry; return $entry->as_string; } #pod =method required_modules #pod #pod This method returns a list of all the modules for which requirements have been #pod specified. #pod #pod =cut sub required_modules { keys %{ $_[0]{requirements} } } #pod =method clone #pod #pod $req->clone; #pod #pod This method returns a clone of the invocant. The clone and the original object #pod can then be changed independent of one another. #pod #pod =cut sub clone { my ($self) = @_; my $new = (ref $self)->new; return $new->add_requirements($self); } sub __entry_for { $_[0]{requirements}{ $_[1] } } sub __modify_entry_for { my ($self, $name, $method, $version) = @_; my $fin = $self->is_finalized; my $old = $self->__entry_for($name); Carp::confess("can't add new requirements to finalized requirements") if $fin and not $old; my $new = ($old || 'CPAN::Meta::Requirements::_Range::Range') ->$method($version); Carp::confess("can't modify finalized requirements") if $fin and $old->as_string ne $new->as_string; $self->{requirements}{ $name } = $new; } #pod =method is_simple #pod #pod This method returns true if and only if all requirements are inclusive minimums #pod -- that is, if their string expression is just the version number. #pod #pod =cut sub is_simple { my ($self) = @_; for my $module ($self->required_modules) { # XXX: This is a complete hack, but also entirely correct. return if $self->__entry_for($module)->as_string =~ /\s/; } return 1; } #pod =method is_finalized #pod #pod This method returns true if the requirements have been finalized by having the #pod C method called on them. #pod #pod =cut sub is_finalized { $_[0]{finalized} } #pod =method finalize #pod #pod This method marks the requirements finalized. Subsequent attempts to change #pod the requirements will be fatal, I they would result in a change. If they #pod would not alter the requirements, they have no effect. #pod #pod If a finalized set of requirements is cloned, the cloned requirements are not #pod also finalized. #pod #pod =cut sub finalize { $_[0]{finalized} = 1 } #pod =method as_string_hash #pod #pod This returns a reference to a hash describing the requirements using the #pod strings in the L specification. #pod #pod For example after the following program: #pod #pod my $req = CPAN::Meta::Requirements->new; #pod #pod $req->add_minimum('CPAN::Meta::Requirements' => 0.102); #pod #pod $req->add_minimum('Library::Foo' => 1.208); #pod #pod $req->add_maximum('Library::Foo' => 2.602); #pod #pod $req->add_minimum('Module::Bar' => 'v1.2.3'); #pod #pod $req->add_exclusion('Module::Bar' => 'v1.2.8'); #pod #pod $req->exact_version('Xyzzy' => '6.01'); #pod #pod my $hashref = $req->as_string_hash; #pod #pod C<$hashref> would contain: #pod #pod { #pod 'CPAN::Meta::Requirements' => '0.102', #pod 'Library::Foo' => '>= 1.208, <= 2.206', #pod 'Module::Bar' => '>= v1.2.3, != v1.2.8', #pod 'Xyzzy' => '== 6.01', #pod } #pod #pod =cut sub as_string_hash { my ($self) = @_; my %hash = map {; $_ => $self->{requirements}{$_}->as_string } $self->required_modules; return \%hash; } #pod =method add_string_requirement #pod #pod $req->add_string_requirement('Library::Foo' => '>= 1.208, <= 2.206'); #pod $req->add_string_requirement('Library::Foo' => v1.208); #pod #pod This method parses the passed in string and adds the appropriate requirement #pod for the given module. A version can be a Perl "v-string". It understands #pod version ranges as described in the L. For #pod example: #pod #pod =over 4 #pod #pod =item 1.3 #pod #pod =item >= 1.3 #pod #pod =item <= 1.3 #pod #pod =item == 1.3 #pod #pod =item != 1.3 #pod #pod =item > 1.3 #pod #pod =item < 1.3 #pod #pod =item >= 1.3, != 1.5, <= 2.0 #pod #pod A version number without an operator is equivalent to specifying a minimum #pod (C=>). Extra whitespace is allowed. #pod #pod =back #pod #pod =cut my %methods_for_op = ( '==' => [ qw(exact_version) ], '!=' => [ qw(add_exclusion) ], '>=' => [ qw(add_minimum) ], '<=' => [ qw(add_maximum) ], '>' => [ qw(add_minimum add_exclusion) ], '<' => [ qw(add_maximum add_exclusion) ], ); sub add_string_requirement { my ($self, $module, $req) = @_; unless ( defined $req && length $req ) { $req = 0; $self->_blank_carp($module); } my $magic = _find_magic_vstring( $req ); if (length $magic) { $self->add_minimum($module => $magic); return; } my @parts = split qr{\s*,\s*}, $req; for my $part (@parts) { my ($op, $ver) = $part =~ m{\A\s*(==|>=|>|<=|<|!=)\s*(.*)\z}; if (! defined $op) { $self->add_minimum($module => $part); } else { Carp::confess("illegal requirement string: $req") unless my $methods = $methods_for_op{ $op }; $self->$_($module => $ver) for @$methods; } } } #pod =method from_string_hash #pod #pod my $req = CPAN::Meta::Requirements->from_string_hash( \%hash ); #pod my $req = CPAN::Meta::Requirements->from_string_hash( \%hash, \%opts ); #pod #pod This is an alternate constructor for a CPAN::Meta::Requirements #pod object. It takes a hash of module names and version requirement #pod strings and returns a new CPAN::Meta::Requirements object. As with #pod add_string_requirement, a version can be a Perl "v-string". Optionally, #pod you can supply a hash-reference of options, exactly as with the L #pod method. #pod #pod =cut sub _blank_carp { my ($self, $module) = @_; Carp::carp("Undefined requirement for $module treated as '0'"); } sub from_string_hash { my ($class, $hash, $options) = @_; my $self = $class->new($options); for my $module (keys %$hash) { my $req = $hash->{$module}; unless ( defined $req && length $req ) { $req = 0; $class->_blank_carp($module); } $self->add_string_requirement($module, $req); } return $self; } ############################################################## { package CPAN::Meta::Requirements::_Range::Exact; sub _new { bless { version => $_[1] } => $_[0] } sub _accepts { return $_[0]{version} == $_[1] } sub as_string { return "== $_[0]{version}" } sub as_modifiers { return [ [ exact_version => $_[0]{version} ] ] } sub _clone { (ref $_[0])->_new( version->new( $_[0]{version} ) ) } sub with_exact_version { my ($self, $version) = @_; return $self->_clone if $self->_accepts($version); Carp::confess("illegal requirements: unequal exact version specified"); } sub with_minimum { my ($self, $minimum) = @_; return $self->_clone if $self->{version} >= $minimum; Carp::confess("illegal requirements: minimum above exact specification"); } sub with_maximum { my ($self, $maximum) = @_; return $self->_clone if $self->{version} <= $maximum; Carp::confess("illegal requirements: maximum below exact specification"); } sub with_exclusion { my ($self, $exclusion) = @_; return $self->_clone unless $exclusion == $self->{version}; Carp::confess("illegal requirements: excluded exact specification"); } } ############################################################## { package CPAN::Meta::Requirements::_Range::Range; sub _self { ref($_[0]) ? $_[0] : (bless { } => $_[0]) } sub _clone { return (bless { } => $_[0]) unless ref $_[0]; my ($s) = @_; my %guts = ( (exists $s->{minimum} ? (minimum => version->new($s->{minimum})) : ()), (exists $s->{maximum} ? (maximum => version->new($s->{maximum})) : ()), (exists $s->{exclusions} ? (exclusions => [ map { version->new($_) } @{ $s->{exclusions} } ]) : ()), ); bless \%guts => ref($s); } sub as_modifiers { my ($self) = @_; my @mods; push @mods, [ add_minimum => $self->{minimum} ] if exists $self->{minimum}; push @mods, [ add_maximum => $self->{maximum} ] if exists $self->{maximum}; push @mods, map {; [ add_exclusion => $_ ] } @{$self->{exclusions} || []}; return \@mods; } sub as_string { my ($self) = @_; return 0 if ! keys %$self; return "$self->{minimum}" if (keys %$self) == 1 and exists $self->{minimum}; my @exclusions = @{ $self->{exclusions} || [] }; my @parts; for my $pair ( [ qw( >= > minimum ) ], [ qw( <= < maximum ) ], ) { my ($op, $e_op, $k) = @$pair; if (exists $self->{$k}) { my @new_exclusions = grep { $_ != $self->{ $k } } @exclusions; if (@new_exclusions == @exclusions) { push @parts, "$op $self->{ $k }"; } else { push @parts, "$e_op $self->{ $k }"; @exclusions = @new_exclusions; } } } push @parts, map {; "!= $_" } @exclusions; return join q{, }, @parts; } sub with_exact_version { my ($self, $version) = @_; $self = $self->_clone; Carp::confess("illegal requirements: exact specification outside of range") unless $self->_accepts($version); return CPAN::Meta::Requirements::_Range::Exact->_new($version); } sub _simplify { my ($self) = @_; if (defined $self->{minimum} and defined $self->{maximum}) { if ($self->{minimum} == $self->{maximum}) { Carp::confess("illegal requirements: excluded all values") if grep { $_ == $self->{minimum} } @{ $self->{exclusions} || [] }; return CPAN::Meta::Requirements::_Range::Exact->_new($self->{minimum}) } Carp::confess("illegal requirements: minimum exceeds maximum") if $self->{minimum} > $self->{maximum}; } # eliminate irrelevant exclusions if ($self->{exclusions}) { my %seen; @{ $self->{exclusions} } = grep { (! defined $self->{minimum} or $_ >= $self->{minimum}) and (! defined $self->{maximum} or $_ <= $self->{maximum}) and ! $seen{$_}++ } @{ $self->{exclusions} }; } return $self; } sub with_minimum { my ($self, $minimum) = @_; $self = $self->_clone; if (defined (my $old_min = $self->{minimum})) { $self->{minimum} = (sort { $b cmp $a } ($minimum, $old_min))[0]; } else { $self->{minimum} = $minimum; } return $self->_simplify; } sub with_maximum { my ($self, $maximum) = @_; $self = $self->_clone; if (defined (my $old_max = $self->{maximum})) { $self->{maximum} = (sort { $a cmp $b } ($maximum, $old_max))[0]; } else { $self->{maximum} = $maximum; } return $self->_simplify; } sub with_exclusion { my ($self, $exclusion) = @_; $self = $self->_clone; push @{ $self->{exclusions} ||= [] }, $exclusion; return $self->_simplify; } sub _accepts { my ($self, $version) = @_; return if defined $self->{minimum} and $version < $self->{minimum}; return if defined $self->{maximum} and $version > $self->{maximum}; return if defined $self->{exclusions} and grep { $version == $_ } @{ $self->{exclusions} }; return 1; } } 1; # vim: ts=2 sts=2 sw=2 et: __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Requirements - a set of version requirements for a CPAN dist =head1 VERSION version 2.131 =head1 SYNOPSIS use CPAN::Meta::Requirements; my $build_requires = CPAN::Meta::Requirements->new; $build_requires->add_minimum('Library::Foo' => 1.208); $build_requires->add_minimum('Library::Foo' => 2.602); $build_requires->add_minimum('Module::Bar' => 'v1.2.3'); $METAyml->{build_requires} = $build_requires->as_string_hash; =head1 DESCRIPTION A CPAN::Meta::Requirements object models a set of version constraints like those specified in the F or F files in CPAN distributions, and as defined by L; It can be built up by adding more and more constraints, and it will reduce them to the simplest representation. Logically impossible constraints will be identified immediately by thrown exceptions. =head1 METHODS =head2 new my $req = CPAN::Meta::Requirements->new; This returns a new CPAN::Meta::Requirements object. It takes an optional hash reference argument. Currently, only one key is supported: =over 4 =item * C -- if provided, when a version cannot be parsed into a version object, this code reference will be called with the invalid version string as first argument, and the module name as second argument. It must return a valid version object. =back All other keys are ignored. =head2 add_minimum $req->add_minimum( $module => $version ); This adds a new minimum version requirement. If the new requirement is redundant to the existing specification, this has no effect. Minimum requirements are inclusive. C<$version> is required, along with any greater version number. This method returns the requirements object. =head2 add_maximum $req->add_maximum( $module => $version ); This adds a new maximum version requirement. If the new requirement is redundant to the existing specification, this has no effect. Maximum requirements are inclusive. No version strictly greater than the given version is allowed. This method returns the requirements object. =head2 add_exclusion $req->add_exclusion( $module => $version ); This adds a new excluded version. For example, you might use these three method calls: $req->add_minimum( $module => '1.00' ); $req->add_maximum( $module => '1.82' ); $req->add_exclusion( $module => '1.75' ); Any version between 1.00 and 1.82 inclusive would be acceptable, except for 1.75. This method returns the requirements object. =head2 exact_version $req->exact_version( $module => $version ); This sets the version required for the given module to I the given version. No other version would be considered acceptable. This method returns the requirements object. =head2 add_requirements $req->add_requirements( $another_req_object ); This method adds all the requirements in the given CPAN::Meta::Requirements object to the requirements object on which it was called. If there are any conflicts, an exception is thrown. This method returns the requirements object. =head2 accepts_module my $bool = $req->accepts_module($module => $version); Given an module and version, this method returns true if the version specification for the module accepts the provided version. In other words, given: Module => '>= 1.00, < 2.00' We will accept 1.00 and 1.75 but not 0.50 or 2.00. For modules that do not appear in the requirements, this method will return true. =head2 clear_requirement $req->clear_requirement( $module ); This removes the requirement for a given module from the object. This method returns the requirements object. =head2 requirements_for_module $req->requirements_for_module( $module ); This returns a string containing the version requirements for a given module in the format described in L or undef if the given module has no requirements. This should only be used for informational purposes such as error messages and should not be interpreted or used for comparison (see L instead.) =head2 required_modules This method returns a list of all the modules for which requirements have been specified. =head2 clone $req->clone; This method returns a clone of the invocant. The clone and the original object can then be changed independent of one another. =head2 is_simple This method returns true if and only if all requirements are inclusive minimums -- that is, if their string expression is just the version number. =head2 is_finalized This method returns true if the requirements have been finalized by having the C method called on them. =head2 finalize This method marks the requirements finalized. Subsequent attempts to change the requirements will be fatal, I they would result in a change. If they would not alter the requirements, they have no effect. If a finalized set of requirements is cloned, the cloned requirements are not also finalized. =head2 as_string_hash This returns a reference to a hash describing the requirements using the strings in the L specification. For example after the following program: my $req = CPAN::Meta::Requirements->new; $req->add_minimum('CPAN::Meta::Requirements' => 0.102); $req->add_minimum('Library::Foo' => 1.208); $req->add_maximum('Library::Foo' => 2.602); $req->add_minimum('Module::Bar' => 'v1.2.3'); $req->add_exclusion('Module::Bar' => 'v1.2.8'); $req->exact_version('Xyzzy' => '6.01'); my $hashref = $req->as_string_hash; C<$hashref> would contain: { 'CPAN::Meta::Requirements' => '0.102', 'Library::Foo' => '>= 1.208, <= 2.206', 'Module::Bar' => '>= v1.2.3, != v1.2.8', 'Xyzzy' => '== 6.01', } =head2 add_string_requirement $req->add_string_requirement('Library::Foo' => '>= 1.208, <= 2.206'); $req->add_string_requirement('Library::Foo' => v1.208); This method parses the passed in string and adds the appropriate requirement for the given module. A version can be a Perl "v-string". It understands version ranges as described in the L. For example: =over 4 =item 1.3 =item >= 1.3 =item <= 1.3 =item == 1.3 =item != 1.3 =item > 1.3 =item < 1.3 =item >= 1.3, != 1.5, <= 2.0 A version number without an operator is equivalent to specifying a minimum (C=>). Extra whitespace is allowed. =back =head2 from_string_hash my $req = CPAN::Meta::Requirements->from_string_hash( \%hash ); my $req = CPAN::Meta::Requirements->from_string_hash( \%hash, \%opts ); This is an alternate constructor for a CPAN::Meta::Requirements object. It takes a hash of module names and version requirement strings and returns a new CPAN::Meta::Requirements object. As with add_string_requirement, a version can be a Perl "v-string". Optionally, you can supply a hash-reference of options, exactly as with the L method. =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/dagolden/CPAN-Meta-Requirements.git =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 CONTRIBUTORS =for stopwords Ed J Karen Etheridge Leon Timmermans robario =over 4 =item * Ed J =item * Karen Etheridge =item * Leon Timmermans =item * robario =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_REQUIREMENTS $fatpacked{"CPAN/Meta/Spec.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_SPEC'; # XXX RULES FOR PATCHING THIS FILE XXX # Patches that fix typos or formatting are acceptable. Patches # that change semantics are not acceptable without prior approval # by David Golden or Ricardo Signes. use 5.006; use strict; use warnings; package CPAN::Meta::Spec; # VERSION $CPAN::Meta::Spec::VERSION = '2.143240'; 1; # ABSTRACT: specification for CPAN distribution metadata # vi:tw=72 __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Spec - specification for CPAN distribution metadata =head1 VERSION version 2.143240 =head1 SYNOPSIS my $distmeta = { name => 'Module-Build', abstract => 'Build and install Perl modules', description => "Module::Build is a system for " . "building, testing, and installing Perl modules. " . "It is meant to ... blah blah blah ...", version => '0.36', release_status => 'stable', author => [ 'Ken Williams ', 'Module-Build List ', # additional contact ], license => [ 'perl_5' ], prereqs => { runtime => { requires => { 'perl' => '5.006', 'ExtUtils::Install' => '0', 'File::Basename' => '0', 'File::Compare' => '0', 'IO::File' => '0', }, recommends => { 'Archive::Tar' => '1.00', 'ExtUtils::Install' => '0.3', 'ExtUtils::ParseXS' => '2.02', }, }, build => { requires => { 'Test::More' => '0', }, } }, resources => { license => ['http://dev.perl.org/licenses/'], }, optional_features => { domination => { description => 'Take over the world', prereqs => { develop => { requires => { 'Genius::Evil' => '1.234' } }, runtime => { requires => { 'Machine::Weather' => '2.0' } }, }, }, }, dynamic_config => 1, keywords => [ qw/ toolchain cpan dual-life / ], 'meta-spec' => { version => '2', url => 'https://metacpan.org/pod/CPAN::Meta::Spec', }, generated_by => 'Module::Build version 0.36', }; =head1 DESCRIPTION This document describes version 2 of the CPAN distribution metadata specification, also known as the "CPAN Meta Spec". Revisions of this specification for typo corrections and prose clarifications may be issued as CPAN::Meta::Spec 2.I. These revisions will never change semantics or add or remove specified behavior. Distribution metadata describe important properties of Perl distributions. Distribution building tools like Module::Build, Module::Install, ExtUtils::MakeMaker or Dist::Zilla should create a metadata file in accordance with this specification and include it with the distribution for use by automated tools that index, examine, package or install Perl distributions. =head1 TERMINOLOGY =over 4 =item distribution This is the primary object described by the metadata. In the context of this document it usually refers to a collection of modules, scripts, and/or documents that are distributed together for other developers to use. Examples of distributions are C, C, or C. =item module This refers to a reusable library of code contained in a single file. Modules usually contain one or more packages and are often referred to by the name of a primary package that can be mapped to the file name. For example, one might refer to C instead of F =item package This refers to a namespace declared with the Perl C statement. In Perl, packages often have a version number property given by the C<$VERSION> variable in the namespace. =item consumer This refers to code that reads a metadata file, deserializes it into a data structure in memory, or interprets a data structure of metadata elements. =item producer This refers to code that constructs a metadata data structure, serializes into a bytestream and/or writes it to disk. =item must, should, may, etc. These terms are interpreted as described in IETF RFC 2119. =back =head1 DATA TYPES Fields in the L section describe data elements, each of which has an associated data type as described herein. There are four primitive types: Boolean, String, List and Map. Other types are subtypes of primitives and define compound data structures or define constraints on the values of a data element. =head2 Boolean A I is used to provide a true or false value. It B be represented as a defined value. =head2 String A I is data element containing a non-zero length sequence of Unicode characters, such as an ordinary Perl scalar that is not a reference. =head2 List A I is an ordered collection of zero or more data elements. Elements of a List may be of mixed types. Producers B represent List elements using a data structure which unambiguously indicates that multiple values are possible, such as a reference to a Perl array (an "arrayref"). Consumers expecting a List B consider a String as equivalent to a List of length 1. =head2 Map A I is an unordered collection of zero or more data elements ("values"), indexed by associated String elements ("keys"). The Map's value elements may be of mixed types. =head2 License String A I is a subtype of String with a restricted set of values. Valid values are described in detail in the description of the L field. =head2 URL I is a subtype of String containing a Uniform Resource Locator or Identifier. [ This type is called URL and not URI for historical reasons. ] =head2 Version A I is a subtype of String containing a value that describes the version number of packages or distributions. Restrictions on format are described in detail in the L section. =head2 Version Range The I type is a subtype of String. It describes a range of Versions that may be present or installed to fulfill prerequisites. It is specified in detail in the L section. =head1 STRUCTURE The metadata structure is a data element of type Map. This section describes valid keys within the Map. Any keys not described in this specification document (whether top-level or within compound data structures described herein) are considered I and B begin with an "x" or "X" and be followed by an underscore; i.e. they must match the pattern: C<< qr{\Ax_}i >>. If a custom key refers to a compound data structure, subkeys within it do not need an "x_" or "X_" prefix. Consumers of metadata may ignore any or all custom keys. All other keys not described herein are invalid and should be ignored by consumers. Producers must not generate or output invalid keys. For each key, an example is provided followed by a description. The description begins with the version of spec in which the key was added or in which the definition was modified, whether the key is I or I and the data type of the corresponding data element. These items are in parentheses, brackets and braces, respectively. If a data type is a Map or Map subtype, valid subkeys will be described as well. Some fields are marked I. These are shown for historical context and must not be produced in or consumed from any metadata structure of version 2 or higher. =head2 REQUIRED FIELDS =head3 abstract Example: abstract => 'Build and install Perl modules' (Spec 1.2) [required] {String} This is a short description of the purpose of the distribution. =head3 author Example: author => [ 'Ken Williams ' ] (Spec 1.2) [required] {List of one or more Strings} This List indicates the person(s) to contact concerning the distribution. The preferred form of the contact string is: contact-name This field provides a general contact list independent of other structured fields provided within the L field, such as C. The addressee(s) can be contacted for any purpose including but not limited to (security) problems with the distribution, questions about the distribution or bugs in the distribution. A distribution's original author is usually the contact listed within this field. Co-maintainers, successor maintainers or mailing lists devoted to the distribution may also be listed in addition to or instead of the original author. =head3 dynamic_config Example: dynamic_config => 1 (Spec 2) [required] {Boolean} A boolean flag indicating whether a F or F (or similar) must be executed to determine prerequisites. This field should be set to a true value if the distribution performs some dynamic configuration (asking questions, sensing the environment, etc.) as part of its configuration. This field should be set to a false value to indicate that prerequisites included in metadata may be considered final and valid for static analysis. Note: when this field is true, post-configuration prerequisites are not guaranteed to bear any relation whatsoever to those stated in the metadata, and relying on them doing so is an error. See also L in the implementors' notes. This field explicitly B indicate whether installation may be safely performed without using a Makefile or Build file, as there may be special files to install or custom installation targets (e.g. for dual-life modules that exist on CPAN as well as in the Perl core). This field only defines whether or not prerequisites are exactly as given in the metadata. =head3 generated_by Example: generated_by => 'Module::Build version 0.36' (Spec 1.0) [required] {String} This field indicates the tool that was used to create this metadata. There are no defined semantics for this field, but it is traditional to use a string in the form "Generating::Package version 1.23" or the author's name, if the file was generated by hand. =head3 license Example: license => [ 'perl_5' ] license => [ 'apache_2_0', 'mozilla_1_0' ] (Spec 2) [required] {List of one or more License Strings} One or more licenses that apply to some or all of the files in the distribution. If multiple licenses are listed, the distribution documentation should be consulted to clarify the interpretation of multiple licenses. The following list of license strings are valid: string description ------------- ----------------------------------------------- agpl_3 GNU Affero General Public License, Version 3 apache_1_1 Apache Software License, Version 1.1 apache_2_0 Apache License, Version 2.0 artistic_1 Artistic License, (Version 1) artistic_2 Artistic License, Version 2.0 bsd BSD License (three-clause) freebsd FreeBSD License (two-clause) gfdl_1_2 GNU Free Documentation License, Version 1.2 gfdl_1_3 GNU Free Documentation License, Version 1.3 gpl_1 GNU General Public License, Version 1 gpl_2 GNU General Public License, Version 2 gpl_3 GNU General Public License, Version 3 lgpl_2_1 GNU Lesser General Public License, Version 2.1 lgpl_3_0 GNU Lesser General Public License, Version 3.0 mit MIT (aka X11) License mozilla_1_0 Mozilla Public License, Version 1.0 mozilla_1_1 Mozilla Public License, Version 1.1 openssl OpenSSL License perl_5 The Perl 5 License (Artistic 1 & GPL 1 or later) qpl_1_0 Q Public License, Version 1.0 ssleay Original SSLeay License sun Sun Internet Standards Source License (SISSL) zlib zlib License The following license strings are also valid and indicate other licensing not described above: string description ------------- ----------------------------------------------- open_source Other Open Source Initiative (OSI) approved license restricted Requires special permission from copyright holder unrestricted Not an OSI approved license, but not restricted unknown License not provided in metadata All other strings are invalid in the license field. =head3 meta-spec Example: 'meta-spec' => { version => '2', url => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec', } (Spec 1.2) [required] {Map} This field indicates the version of the CPAN Meta Spec that should be used to interpret the metadata. Consumers must check this key as soon as possible and abort further metadata processing if the meta-spec version is not supported by the consumer. The following keys are valid, but only C is required. =over =item version This subkey gives the integer I of the CPAN Meta Spec against which the document was generated. =item url This is a I of the metadata specification document corresponding to the given version. This is strictly for human-consumption and should not impact the interpretation of the document. For the version 2 spec, either of these are recommended: =over 4 =item * C =item * C =back =back =head3 name Example: name => 'Module-Build' (Spec 1.0) [required] {String} This field is the name of the distribution. This is often created by taking the "main package" in the distribution and changing C<::> to C<->, but the name may be completely unrelated to the packages within the distribution. For example, L is distributed as part of the distribution name "libwww-perl". =head3 release_status Example: release_status => 'stable' (Spec 2) [required] {String} This field provides the release status of this distribution. If the C field contains an underscore character, then C B be "stable." The C field B have one of the following values: =over =item stable This indicates an ordinary, "final" release that should be indexed by PAUSE or other indexers. =item testing This indicates a "beta" release that is substantially complete, but has an elevated risk of bugs and requires additional testing. The distribution should not be installed over a stable release without an explicit request or other confirmation from a user. This release status may also be used for "release candidate" versions of a distribution. =item unstable This indicates an "alpha" release that is under active development, but has been released for early feedback or testing and may be missing features or may have serious bugs. The distribution should not be installed over a stable release without an explicit request or other confirmation from a user. =back Consumers B use this field to determine how to index the distribution for CPAN or other repositories in addition to or in replacement of heuristics based on version number or file name. =head3 version Example: version => '0.36' (Spec 1.0) [required] {Version} This field gives the version of the distribution to which the metadata structure refers. =head2 OPTIONAL FIELDS =head3 description Example: description => "Module::Build is a system for " . "building, testing, and installing Perl modules. " . "It is meant to ... blah blah blah ...", (Spec 2) [optional] {String} A longer, more complete description of the purpose or intended use of the distribution than the one provided by the C key. =head3 keywords Example: keywords => [ qw/ toolchain cpan dual-life / ] (Spec 1.1) [optional] {List of zero or more Strings} A List of keywords that describe this distribution. Keywords B include whitespace. =head3 no_index Example: no_index => { file => [ 'My/Module.pm' ], directory => [ 'My/Private' ], package => [ 'My::Module::Secret' ], namespace => [ 'My::Module::Sample' ], } (Spec 1.2) [optional] {Map} This Map describes any files, directories, packages, and namespaces that are private to the packaging or implementation of the distribution and should be ignored by indexing or search tools. Note that this is a list of exclusions, and the spec does not define what to I - see L in the implementors notes for more information. Valid subkeys are as follows: =over =item file A I of relative paths to files. Paths B specified with unix conventions. =item directory A I of relative paths to directories. Paths B specified with unix conventions. [ Note: previous editions of the spec had C instead of C ] =item package A I of package names. =item namespace A I of package namespaces, where anything below the namespace must be ignored, but I the namespace itself. In the example above for C, C would be ignored, but C would not. =back =head3 optional_features Example: optional_features => { sqlite => { description => 'Provides SQLite support', prereqs => { runtime => { requires => { 'DBD::SQLite' => '1.25' } } } } } (Spec 2) [optional] {Map} This Map describes optional features with incremental prerequisites. Each key of the C Map is a String used to identify the feature and each value is a Map with additional information about the feature. Valid subkeys include: =over =item description This is a String describing the feature. Every optional feature should provide a description =item prereqs This entry is required and has the same structure as that of the C> key. It provides a list of package requirements that must be satisfied for the feature to be supported or enabled. There is one crucial restriction: the prereqs of an optional feature B include C phase prereqs. =back Consumers B include optional features as prerequisites without explicit instruction from users (whether via interactive prompting, a function parameter or a configuration value, etc. ). If an optional feature is used by a consumer to add additional prerequisites, the consumer should merge the optional feature prerequisites into those given by the C key using the same semantics. See L for details on merging prerequisites. I Because there is currently no way for a distribution to specify a dependency on an optional feature of another dependency, the use of C is discouraged. Instead, create a separate, installable distribution that ensures the desired feature is available. For example, if C has a C feature, release a separate C distribution that satisfies requirements for the feature. =head3 prereqs Example: prereqs => { runtime => { requires => { 'perl' => '5.006', 'File::Spec' => '0.86', 'JSON' => '2.16', }, recommends => { 'JSON::XS' => '2.26', }, suggests => { 'Archive::Tar' => '0', }, }, build => { requires => { 'Alien::SDL' => '1.00', }, }, test => { recommends => { 'Test::Deep' => '0.10', }, } } (Spec 2) [optional] {Map} This is a Map that describes all the prerequisites of the distribution. The keys are phases of activity, such as C, C, C or C. Values are Maps in which the keys name the type of prerequisite relationship such as C, C, or C and the value provides a set of prerequisite relations. The set of relations B be specified as a Map of package names to version ranges. The full definition for this field is given in the L section. =head3 provides Example: provides => { 'Foo::Bar' => { file => 'lib/Foo/Bar.pm', version => '0.27_02', }, 'Foo::Bar::Blah' => { file => 'lib/Foo/Bar/Blah.pm', }, 'Foo::Bar::Baz' => { file => 'lib/Foo/Bar/Baz.pm', version => '0.3', }, } (Spec 1.2) [optional] {Map} This describes all packages provided by this distribution. This information is used by distribution and automation mechanisms like PAUSE, CPAN, metacpan.org and search.cpan.org to build indexes saying in which distribution various packages can be found. The keys of C are package names that can be found within the distribution. If a package name key is provided, it must have a Map with the following valid subkeys: =over =item file This field is required. It must contain a Unix-style relative file path from the root of the distribution directory to a file that contains or generates the package. It may be given as C or C to claim a package for indexing without needing a C<*.pm>. =item version If it exists, this field must contains a I String for the package. If the package does not have a C<$VERSION>, this field must be omitted. =back =head3 resources Example: resources => { license => [ 'http://dev.perl.org/licenses/' ], homepage => 'http://sourceforge.net/projects/module-build', bugtracker => { web => 'http://rt.cpan.org/Public/Dist/Display.html?Name=CPAN-Meta', mailto => 'meta-bugs@example.com', }, repository => { url => 'git://github.com/dagolden/cpan-meta.git', web => 'http://github.com/dagolden/cpan-meta', type => 'git', }, x_twitter => 'http://twitter.com/cpan_linked/', } (Spec 2) [optional] {Map} This field describes resources related to this distribution. Valid subkeys include: =over =item homepage The official home of this project on the web. =item license A List of I's that relate to this distribution's license. As with the top-level C field, distribution documentation should be consulted to clarify the interpretation of multiple licenses provided here. =item bugtracker This entry describes the bug tracking system for this distribution. It is a Map with the following valid keys: web - a URL pointing to a web front-end for the bug tracker mailto - an email address to which bugs can be sent =item repository This entry describes the source control repository for this distribution. It is a Map with the following valid keys: url - a URL pointing to the repository itself web - a URL pointing to a web front-end for the repository type - a lowercase string indicating the VCS used Because a url like C is ambiguous as to type, producers should provide a C whenever a C key is given. The C field should be the name of the most common program used to work with the repository, e.g. C, C, C, C, C or C. =back =head2 DEPRECATED FIELDS =head3 build_requires I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 configure_requires I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 conflicts I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 distribution_type I<(Deprecated in Spec 2)> [optional] {String} This field indicated 'module' or 'script' but was considered meaningless, since many distributions are hybrids of several kinds of things. =head3 license_uri I<(Deprecated in Spec 1.2)> [optional] {URL} Replaced by C in C =head3 private I<(Deprecated in Spec 1.2)> [optional] {Map} This field has been renamed to L. =head3 recommends I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 requires I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head1 VERSION NUMBERS =head2 Version Formats This section defines the Version type, used by several fields in the CPAN Meta Spec. Version numbers must be treated as strings, not numbers. For example, C<1.200> B be serialized as C<1.2>. Version comparison should be delegated to the Perl L module, version 0.80 or newer. Unless otherwise specified, version numbers B appear in one of two formats: =over =item Decimal versions Decimal versions are regular "decimal numbers", with some limitations. They B be non-negative and B begin and end with a digit. A single underscore B be included, but B be between two digits. They B use exponential notation ("1.23e-2"). version => '1.234' # OK version => '1.23_04' # OK version => '1.23_04_05' # Illegal version => '1.' # Illegal version => '.1' # Illegal =item Dotted-integer versions Dotted-integer (also known as dotted-decimal) versions consist of positive integers separated by full stop characters (i.e. "dots", "periods" or "decimal points"). This are equivalent in format to Perl "v-strings", with some additional restrictions on form. They must be given in "normal" form, which has a leading "v" character and at least three integer components. To retain a one-to-one mapping with decimal versions, all components after the first B be restricted to the range 0 to 999. The final component B be separated by an underscore character instead of a period. version => 'v1.2.3' # OK version => 'v1.2_3' # OK version => 'v1.2.3.4' # OK version => 'v1.2.3_4' # OK version => 'v2009.10.31' # OK version => 'v1.2' # Illegal version => '1.2.3' # Illegal version => 'v1.2_3_4' # Illegal version => 'v1.2009.10.31' # Not recommended =back =head2 Version Ranges Some fields (prereq, optional_features) indicate the particular version(s) of some other module that may be required as a prerequisite. This section details the Version Range type used to provide this information. The simplest format for a Version Range is just the version number itself, e.g. C<2.4>. This means that B version 2.4 must be present. To indicate that B version of a prerequisite is okay, even if the prerequisite doesn't define a version at all, use the version C<0>. Alternatively, a version range B use the operators E (less than), E= (less than or equal), E (greater than), E= (greater than or equal), == (equal), and != (not equal). For example, the specification C 2.0> means that any version of the prerequisite less than 2.0 is suitable. For more complicated situations, version specifications B be AND-ed together using commas. The specification C= 1.2, != 1.5, E 2.0> indicates a version that must be B 1.2, B 2.0, and B 1.5. =head1 PREREQUISITES =head2 Prereq Spec The C key in the top-level metadata and within C define the relationship between a distribution and other packages. The prereq spec structure is a hierarchical data structure which divides prerequisites into I of activity in the installation process and I that indicate how prerequisites should be resolved. For example, to specify that C is C during the C phase, this entry would appear in the distribution metadata: prereqs => { test => { requires => { 'Data::Dumper' => '2.00' } } } =head3 Phases Requirements for regular use must be listed in the C phase. Other requirements should be listed in the earliest stage in which they are required and consumers must accumulate and satisfy requirements across phases before executing the activity. For example, C requirements must also be available during the C phase. before action requirements that must be met ---------------- -------------------------------- perl Build.PL configure perl Makefile.PL make configure, runtime, build Build make test configure, runtime, build, test Build test Consumers that install the distribution must ensure that I requirements are also installed and may install dependencies from other phases. after action requirements that must be met ---------------- -------------------------------- make install runtime Build install =over =item configure The configure phase occurs before any dynamic configuration has been attempted. Libraries required by the configure phase B be available for use before the distribution building tool has been executed. =item build The build phase is when the distribution's source code is compiled (if necessary) and otherwise made ready for installation. =item test The test phase is when the distribution's automated test suite is run. Any library that is needed only for testing and not for subsequent use should be listed here. =item runtime The runtime phase refers not only to when the distribution's contents are installed, but also to its continued use. Any library that is a prerequisite for regular use of this distribution should be indicated here. =item develop The develop phase's prereqs are libraries needed to work on the distribution's source code as its author does. These tools might be needed to build a release tarball, to run author-only tests, or to perform other tasks related to developing new versions of the distribution. =back =head3 Relationships =over =item requires These dependencies B be installed for proper completion of the phase. =item recommends Recommended dependencies are I encouraged and should be satisfied except in resource constrained environments. =item suggests These dependencies are optional, but are suggested for enhanced operation of the described distribution. =item conflicts These libraries cannot be installed when the phase is in operation. This is a very rare situation, and the C relationship should be used with great caution, or not at all. =back =head2 Merging and Resolving Prerequisites Whenever metadata consumers merge prerequisites, either from different phases or from C, they should merged in a way which preserves the intended semantics of the prerequisite structure. Generally, this means concatenating the version specifications using commas, as described in the L section. Another subtle error that can occur in resolving prerequisites comes from the way that modules in prerequisites are indexed to distribution files on CPAN. When a module is deleted from a distribution, prerequisites calling for that module could indicate an older distribution should be installed, potentially overwriting files from a newer distribution. For example, as of Oct 31, 2009, the CPAN index file contained these module-distribution mappings: Class::MOP 0.94 D/DR/DROLSKY/Class-MOP-0.94.tar.gz Class::MOP::Class 0.94 D/DR/DROLSKY/Class-MOP-0.94.tar.gz Class::MOP::Class::Immutable 0.04 S/ST/STEVAN/Class-MOP-0.36.tar.gz Consider the case where "Class::MOP" 0.94 is installed. If a distribution specified "Class::MOP::Class::Immutable" as a prerequisite, it could result in Class-MOP-0.36.tar.gz being installed, overwriting any files from Class-MOP-0.94.tar.gz. Consumers of metadata B test whether prerequisites would result in installed module files being "downgraded" to an older version and B warn users or ignore the prerequisite that would cause such a result. =head1 SERIALIZATION Distribution metadata should be serialized (as a hashref) as JSON-encoded data and packaged with distributions as the file F. In the past, the distribution metadata structure had been packed with distributions as F, a file in the YAML Tiny format (for which, see L). Tools that consume distribution metadata from disk should be capable of loading F, but should prefer F if both are found. =head1 NOTES FOR IMPLEMENTORS =head2 Extracting Version Numbers from Perl Modules To get the version number from a Perl module, consumers should use the C<< MM->parse_version($file) >> method provided by L or L. For example, for the module given by C<$mod>, the version may be retrieved in one of the following ways: # via ExtUtils::MakeMaker my $file = MM->_installed_file_for_module($mod); my $version = MM->parse_version($file) The private C<_installed_file_for_module> method may be replaced with other methods for locating a module in C<@INC>. # via Module::Metadata my $info = Module::Metadata->new_from_module($mod); my $version = $info->version; If only a filename is available, the following approach may be used: # via Module::Build my $info = Module::Metadata->new_from_file($file); my $version = $info->version; =head2 Comparing Version Numbers The L module provides the most reliable way to compare version numbers in all the various ways they might be provided or might exist within modules. Given two strings containing version numbers, C<$v1> and C<$v2>, they should be converted to C objects before using ordinary comparison operators. For example: use version; if ( version->new($v1) <=> version->new($v2) ) { print "Versions are not equal\n"; } If the only comparison needed is whether an installed module is of a sufficiently high version, a direct test may be done using the string form of C and the C function. For example, for module C<$mod> and version prerequisite C<$prereq>: if ( eval "use $mod $prereq (); 1" ) { print "Module $mod version is OK.\n"; } If the values of C<$mod> and C<$prereq> have not been scrubbed, however, this presents security implications. =head2 Prerequisites for dynamically configured distributions When C is true, it is an error to presume that the prerequisites given in distribution metadata will have any relationship whatsoever to the actual prerequisites of the distribution. In practice, however, one can generally expect such prerequisites to be one of two things: =over 4 =item * The minimum prerequisites for the distribution, to which dynamic configuration will only add items =item * Whatever the distribution configured with on the releaser's machine at release time =back The second case often turns out to have identical results to the first case, albeit only by accident. As such, consumers may use this data for informational analysis, but presenting it to the user as canonical or relying on it as such is invariably the height of folly. =head2 Indexing distributions a la PAUSE While no_index tells you what must be ignored when indexing, this spec holds no opinion on how you should get your initial candidate list of things to possibly index. For "normal" distributions you might consider simply indexing the contents of lib/, but there are many fascinating oddities on CPAN and many dists from the days when it was normal to put the main .pm file in the root of the distribution archive - so PAUSE currently indexes all .pm and .PL files that are not either (a) specifically excluded by no_index (b) in C, C, or C directories, or common 'mistake' directories such as C. Or: If you're trying to be PAUSE-like, make sure you skip C, C and C as well as anything marked as no_index. Also remember: If the META file contains a provides field, you shouldn't be indexing anything in the first place - just use that. =head1 SEE ALSO =over 4 =item * CPAN, L =item * JSON, L =item * YAML, L =item * L =item * L =item * L =item * L =item * L =back =head1 HISTORY Ken Williams wrote the original CPAN Meta Spec (also known as the "META.yml spec") in 2003 and maintained it through several revisions with input from various members of the community. In 2005, Randy Sims redrafted it from HTML to POD for the version 1.2 release. Ken continued to maintain the spec through version 1.4. In late 2009, David Golden organized the version 2 proposal review process. David and Ricardo Signes drafted the final version 2 spec in April 2010 based on the version 1.4 spec and patches contributed during the proposal process. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_SPEC $fatpacked{"CPAN/Meta/Validator.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_VALIDATOR'; use 5.006; use strict; use warnings; package CPAN::Meta::Validator; # VERSION $CPAN::Meta::Validator::VERSION = '2.143240'; #pod =head1 SYNOPSIS #pod #pod my $struct = decode_json_file('META.json'); #pod #pod my $cmv = CPAN::Meta::Validator->new( $struct ); #pod #pod unless ( $cmv->is_valid ) { #pod my $msg = "Invalid META structure. Errors found:\n"; #pod $msg .= join( "\n", $cmv->errors ); #pod die $msg; #pod } #pod #pod =head1 DESCRIPTION #pod #pod This module validates a CPAN Meta structure against the version of the #pod the specification claimed in the C field of the structure. #pod #pod =cut #--------------------------------------------------------------------------# # This code copied and adapted from Test::CPAN::Meta # by Barbie, for Miss Barbell Productions, # L #--------------------------------------------------------------------------# #--------------------------------------------------------------------------# # Specification Definitions #--------------------------------------------------------------------------# my %known_specs = ( '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html', '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html', '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html', '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html', '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html' ); my %known_urls = map {$known_specs{$_} => $_} keys %known_specs; my $module_map1 = { 'map' => { ':key' => { name => \&module, value => \&exversion } } }; my $module_map2 = { 'map' => { ':key' => { name => \&module, value => \&version } } }; my $no_index_2 = { 'map' => { file => { list => { value => \&string } }, directory => { list => { value => \&string } }, 'package' => { list => { value => \&string } }, namespace => { list => { value => \&string } }, ':key' => { name => \&custom_2, value => \&anything }, } }; my $no_index_1_3 = { 'map' => { file => { list => { value => \&string } }, directory => { list => { value => \&string } }, 'package' => { list => { value => \&string } }, namespace => { list => { value => \&string } }, ':key' => { name => \&string, value => \&anything }, } }; my $no_index_1_2 = { 'map' => { file => { list => { value => \&string } }, dir => { list => { value => \&string } }, 'package' => { list => { value => \&string } }, namespace => { list => { value => \&string } }, ':key' => { name => \&string, value => \&anything }, } }; my $no_index_1_1 = { 'map' => { ':key' => { name => \&string, list => { value => \&string } }, } }; my $prereq_map = { map => { ':key' => { name => \&phase, 'map' => { ':key' => { name => \&relation, %$module_map1, }, }, } }, }; my %definitions = ( '2' => { # REQUIRED 'abstract' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, list => { value => \&string } }, 'dynamic_config' => { mandatory => 1, value => \&boolean }, 'generated_by' => { mandatory => 1, value => \&string }, 'license' => { mandatory => 1, list => { value => \&license } }, 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { value => \&url }, ':key' => { name => \&custom_2, value => \&anything }, } }, 'name' => { mandatory => 1, value => \&string }, 'release_status' => { mandatory => 1, value => \&release_status }, 'version' => { mandatory => 1, value => \&version }, # OPTIONAL 'description' => { value => \&string }, 'keywords' => { list => { value => \&string } }, 'no_index' => $no_index_2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, prereqs => $prereq_map, ':key' => { name => \&custom_2, value => \&anything }, } } } }, 'prereqs' => $prereq_map, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&custom_2, value => \&anything }, } } } }, 'resources' => { 'map' => { license => { list => { value => \&url } }, homepage => { value => \&url }, bugtracker => { 'map' => { web => { value => \&url }, mailto => { value => \&string}, ':key' => { name => \&custom_2, value => \&anything }, } }, repository => { 'map' => { web => { value => \&url }, url => { value => \&url }, type => { value => \&string }, ':key' => { name => \&custom_2, value => \&anything }, } }, ':key' => { value => \&string, name => \&custom_2 }, } }, # CUSTOM -- additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&custom_2, value => \&anything }, }, '1.4' => { 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { mandatory => 1, value => \&urlspec }, ':key' => { name => \&string, value => \&anything }, }, }, 'name' => { mandatory => 1, value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'abstract' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, list => { value => \&string } }, 'license' => { mandatory => 1, value => \&license }, 'generated_by' => { mandatory => 1, value => \&string }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'configure_requires' => $module_map1, 'conflicts' => $module_map2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, requires => $module_map1, recommends => $module_map1, build_requires => $module_map1, conflicts => $module_map2, ':key' => { name => \&string, value => \&anything }, } } } }, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&string, value => \&anything }, } } } }, 'no_index' => $no_index_1_3, 'private' => $no_index_1_3, 'keywords' => { list => { value => \&string } }, 'resources' => { 'map' => { license => { value => \&url }, homepage => { value => \&url }, bugtracker => { value => \&url }, repository => { value => \&url }, ':key' => { value => \&string, name => \&custom_1 }, } }, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, '1.3' => { 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { mandatory => 1, value => \&urlspec }, ':key' => { name => \&string, value => \&anything }, }, }, 'name' => { mandatory => 1, value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'abstract' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, list => { value => \&string } }, 'license' => { mandatory => 1, value => \&license }, 'generated_by' => { mandatory => 1, value => \&string }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, requires => $module_map1, recommends => $module_map1, build_requires => $module_map1, conflicts => $module_map2, ':key' => { name => \&string, value => \&anything }, } } } }, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&string, value => \&anything }, } } } }, 'no_index' => $no_index_1_3, 'private' => $no_index_1_3, 'keywords' => { list => { value => \&string } }, 'resources' => { 'map' => { license => { value => \&url }, homepage => { value => \&url }, bugtracker => { value => \&url }, repository => { value => \&url }, ':key' => { value => \&string, name => \&custom_1 }, } }, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, # v1.2 is misleading, it seems to assume that a number of fields where created # within v1.1, when they were created within v1.2. This may have been an # original mistake, and that a v1.1 was retro fitted into the timeline, when # v1.2 was originally slated as v1.1. But I could be wrong ;) '1.2' => { 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { mandatory => 1, value => \&urlspec }, ':key' => { name => \&string, value => \&anything }, }, }, 'name' => { mandatory => 1, value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'license' => { mandatory => 1, value => \&license }, 'generated_by' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, list => { value => \&string } }, 'abstract' => { mandatory => 1, value => \&string }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'keywords' => { list => { value => \&string } }, 'private' => $no_index_1_2, '$no_index' => $no_index_1_2, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, requires => $module_map1, recommends => $module_map1, build_requires => $module_map1, conflicts => $module_map2, ':key' => { name => \&string, value => \&anything }, } } } }, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&string, value => \&anything }, } } } }, 'resources' => { 'map' => { license => { value => \&url }, homepage => { value => \&url }, bugtracker => { value => \&url }, repository => { value => \&url }, ':key' => { value => \&string, name => \&custom_1 }, } }, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, # note that the 1.1 spec only specifies 'version' as mandatory '1.1' => { 'name' => { value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'license' => { value => \&license }, 'generated_by' => { value => \&string }, 'license_uri' => { value => \&url }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'private' => $no_index_1_1, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, # note that the 1.0 spec doesn't specify optional or mandatory fields # but we will treat version as mandatory since otherwise META 1.0 is # completely arbitrary and pointless '1.0' => { 'name' => { value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'license' => { value => \&license }, 'generated_by' => { value => \&string }, 'license_uri' => { value => \&url }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, ); #--------------------------------------------------------------------------# # Code #--------------------------------------------------------------------------# #pod =method new #pod #pod my $cmv = CPAN::Meta::Validator->new( $struct ) #pod #pod The constructor must be passed a metadata structure. #pod #pod =cut sub new { my ($class,$data) = @_; # create an attributes hash my $self = { 'data' => $data, 'spec' => eval { $data->{'meta-spec'}{'version'} } || "1.0", 'errors' => undef, }; # create the object return bless $self, $class; } #pod =method is_valid #pod #pod if ( $cmv->is_valid ) { #pod ... #pod } #pod #pod Returns a boolean value indicating whether the metadata provided #pod is valid. #pod #pod =cut sub is_valid { my $self = shift; my $data = $self->{data}; my $spec_version = $self->{spec}; $self->check_map($definitions{$spec_version},$data); return ! $self->errors; } #pod =method errors #pod #pod warn( join "\n", $cmv->errors ); #pod #pod Returns a list of errors seen during validation. #pod #pod =cut sub errors { my $self = shift; return () unless(defined $self->{errors}); return @{$self->{errors}}; } #pod =begin :internals #pod #pod =head2 Check Methods #pod #pod =over #pod #pod =item * #pod #pod check_map($spec,$data) #pod #pod Checks whether a map (or hash) part of the data structure conforms to the #pod appropriate specification definition. #pod #pod =item * #pod #pod check_list($spec,$data) #pod #pod Checks whether a list (or array) part of the data structure conforms to #pod the appropriate specification definition. #pod #pod =item * #pod #pod =back #pod #pod =cut my $spec_error = "Missing validation action in specification. " . "Must be one of 'map', 'list', or 'value'"; sub check_map { my ($self,$spec,$data) = @_; if(ref($spec) ne 'HASH') { $self->_error( "Unknown META specification, cannot validate." ); return; } if(ref($data) ne 'HASH') { $self->_error( "Expected a map structure from string or file." ); return; } for my $key (keys %$spec) { next unless($spec->{$key}->{mandatory}); next if(defined $data->{$key}); push @{$self->{stack}}, $key; $self->_error( "Missing mandatory field, '$key'" ); pop @{$self->{stack}}; } for my $key (keys %$data) { push @{$self->{stack}}, $key; if($spec->{$key}) { if($spec->{$key}{value}) { $spec->{$key}{value}->($self,$key,$data->{$key}); } elsif($spec->{$key}{'map'}) { $self->check_map($spec->{$key}{'map'},$data->{$key}); } elsif($spec->{$key}{'list'}) { $self->check_list($spec->{$key}{'list'},$data->{$key}); } else { $self->_error( "$spec_error for '$key'" ); } } elsif ($spec->{':key'}) { $spec->{':key'}{name}->($self,$key,$key); if($spec->{':key'}{value}) { $spec->{':key'}{value}->($self,$key,$data->{$key}); } elsif($spec->{':key'}{'map'}) { $self->check_map($spec->{':key'}{'map'},$data->{$key}); } elsif($spec->{':key'}{'list'}) { $self->check_list($spec->{':key'}{'list'},$data->{$key}); } else { $self->_error( "$spec_error for ':key'" ); } } else { $self->_error( "Unknown key, '$key', found in map structure" ); } pop @{$self->{stack}}; } } sub check_list { my ($self,$spec,$data) = @_; if(ref($data) ne 'ARRAY') { $self->_error( "Expected a list structure" ); return; } if(defined $spec->{mandatory}) { if(!defined $data->[0]) { $self->_error( "Missing entries from mandatory list" ); } } for my $value (@$data) { push @{$self->{stack}}, $value || ""; if(defined $spec->{value}) { $spec->{value}->($self,'list',$value); } elsif(defined $spec->{'map'}) { $self->check_map($spec->{'map'},$value); } elsif(defined $spec->{'list'}) { $self->check_list($spec->{'list'},$value); } elsif ($spec->{':key'}) { $self->check_map($spec,$value); } else { $self->_error( "$spec_error associated with '$self->{stack}[-2]'" ); } pop @{$self->{stack}}; } } #pod =head2 Validator Methods #pod #pod =over #pod #pod =item * #pod #pod header($self,$key,$value) #pod #pod Validates that the header is valid. #pod #pod Note: No longer used as we now read the data structure, not the file. #pod #pod =item * #pod #pod url($self,$key,$value) #pod #pod Validates that a given value is in an acceptable URL format #pod #pod =item * #pod #pod urlspec($self,$key,$value) #pod #pod Validates that the URL to a META specification is a known one. #pod #pod =item * #pod #pod string_or_undef($self,$key,$value) #pod #pod Validates that the value is either a string or an undef value. Bit of a #pod catchall function for parts of the data structure that are completely user #pod defined. #pod #pod =item * #pod #pod string($self,$key,$value) #pod #pod Validates that a string exists for the given key. #pod #pod =item * #pod #pod file($self,$key,$value) #pod #pod Validate that a file is passed for the given key. This may be made more #pod thorough in the future. For now it acts like \&string. #pod #pod =item * #pod #pod exversion($self,$key,$value) #pod #pod Validates a list of versions, e.g. '<= 5, >=2, ==3, !=4, >1, <6, 0'. #pod #pod =item * #pod #pod version($self,$key,$value) #pod #pod Validates a single version string. Versions of the type '5.8.8' and '0.00_00' #pod are both valid. A leading 'v' like 'v1.2.3' is also valid. #pod #pod =item * #pod #pod boolean($self,$key,$value) #pod #pod Validates for a boolean value. Currently these values are '1', '0', 'true', #pod 'false', however the latter 2 may be removed. #pod #pod =item * #pod #pod license($self,$key,$value) #pod #pod Validates that a value is given for the license. Returns 1 if an known license #pod type, or 2 if a value is given but the license type is not a recommended one. #pod #pod =item * #pod #pod custom_1($self,$key,$value) #pod #pod Validates that the given key is in CamelCase, to indicate a user defined #pod keyword and only has characters in the class [-_a-zA-Z]. In version 1.X #pod of the spec, this was only explicitly stated for 'resources'. #pod #pod =item * #pod #pod custom_2($self,$key,$value) #pod #pod Validates that the given key begins with 'x_' or 'X_', to indicate a user #pod defined keyword and only has characters in the class [-_a-zA-Z] #pod #pod =item * #pod #pod identifier($self,$key,$value) #pod #pod Validates that key is in an acceptable format for the META specification, #pod for an identifier, i.e. any that matches the regular expression #pod qr/[a-z][a-z_]/i. #pod #pod =item * #pod #pod module($self,$key,$value) #pod #pod Validates that a given key is in an acceptable module name format, e.g. #pod 'Test::CPAN::Meta::Version'. #pod #pod =back #pod #pod =end :internals #pod #pod =cut sub header { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value && $value =~ /^--- #YAML:1.0/); } $self->_error( "file does not have a valid YAML header." ); return 0; } sub release_status { my ($self,$key,$value) = @_; if(defined $value) { my $version = $self->{data}{version} || ''; if ( $version =~ /_/ ) { return 1 if ( $value =~ /\A(?:testing|unstable)\z/ ); $self->_error( "'$value' for '$key' is invalid for version '$version'" ); } else { return 1 if ( $value =~ /\A(?:stable|testing|unstable)\z/ ); $self->_error( "'$value' for '$key' is invalid" ); } } else { $self->_error( "'$key' is not defined" ); } return 0; } # _uri_split taken from URI::Split by Gisle Aas, Copyright 2003 sub _uri_split { return $_[0] =~ m,(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?,; } sub url { my ($self,$key,$value) = @_; if(defined $value) { my ($scheme, $auth, $path, $query, $frag) = _uri_split($value); unless ( defined $scheme && length $scheme ) { $self->_error( "'$value' for '$key' does not have a URL scheme" ); return 0; } unless ( defined $auth && length $auth ) { $self->_error( "'$value' for '$key' does not have a URL authority" ); return 0; } return 1; } $value ||= ''; $self->_error( "'$value' for '$key' is not a valid URL." ); return 0; } sub urlspec { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value && $known_specs{$self->{spec}} eq $value); if($value && $known_urls{$value}) { $self->_error( 'META specification URL does not match version' ); return 0; } } $self->_error( 'Unknown META specification' ); return 0; } sub anything { return 1 } sub string { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value || $value =~ /^0$/); } $self->_error( "value is an undefined string" ); return 0; } sub string_or_undef { my ($self,$key,$value) = @_; return 1 unless(defined $value); return 1 if($value || $value =~ /^0$/); $self->_error( "No string defined for '$key'" ); return 0; } sub file { my ($self,$key,$value) = @_; return 1 if(defined $value); $self->_error( "No file defined for '$key'" ); return 0; } sub exversion { my ($self,$key,$value) = @_; if(defined $value && ($value || $value =~ /0/)) { my $pass = 1; for(split(",",$value)) { $self->version($key,$_) or ($pass = 0); } return $pass; } $value = '' unless(defined $value); $self->_error( "'$value' for '$key' is not a valid version." ); return 0; } sub version { my ($self,$key,$value) = @_; if(defined $value) { return 0 unless($value || $value =~ /0/); return 1 if($value =~ /^\s*((<|<=|>=|>|!=|==)\s*)?v?\d+((\.\d+((_|\.)\d+)?)?)/); } else { $value = ''; } $self->_error( "'$value' for '$key' is not a valid version." ); return 0; } sub boolean { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value =~ /^(0|1|true|false)$/); } else { $value = ''; } $self->_error( "'$value' for '$key' is not a boolean value." ); return 0; } my %v1_licenses = ( 'perl' => 'http://dev.perl.org/licenses/', 'gpl' => 'http://www.opensource.org/licenses/gpl-license.php', 'apache' => 'http://apache.org/licenses/LICENSE-2.0', 'artistic' => 'http://opensource.org/licenses/artistic-license.php', 'artistic_2' => 'http://opensource.org/licenses/artistic-license-2.0.php', 'lgpl' => 'http://www.opensource.org/licenses/lgpl-license.php', 'bsd' => 'http://www.opensource.org/licenses/bsd-license.php', 'gpl' => 'http://www.opensource.org/licenses/gpl-license.php', 'mit' => 'http://opensource.org/licenses/mit-license.php', 'mozilla' => 'http://opensource.org/licenses/mozilla1.1.php', 'open_source' => undef, 'unrestricted' => undef, 'restrictive' => undef, 'unknown' => undef, ); my %v2_licenses = map { $_ => 1 } qw( agpl_3 apache_1_1 apache_2_0 artistic_1 artistic_2 bsd freebsd gfdl_1_2 gfdl_1_3 gpl_1 gpl_2 gpl_3 lgpl_2_1 lgpl_3_0 mit mozilla_1_0 mozilla_1_1 openssl perl_5 qpl_1_0 ssleay sun zlib open_source restricted unrestricted unknown ); sub license { my ($self,$key,$value) = @_; my $licenses = $self->{spec} < 2 ? \%v1_licenses : \%v2_licenses; if(defined $value) { return 1 if($value && exists $licenses->{$value}); } else { $value = ''; } $self->_error( "License '$value' is invalid" ); return 0; } sub custom_1 { my ($self,$key) = @_; if(defined $key) { # a valid user defined key should be alphabetic # and contain at least one capital case letter. return 1 if($key && $key =~ /^[_a-z]+$/i && $key =~ /[A-Z]/); } else { $key = ''; } $self->_error( "Custom resource '$key' must be in CamelCase." ); return 0; } sub custom_2 { my ($self,$key) = @_; if(defined $key) { return 1 if($key && $key =~ /^x_/i); # user defined } else { $key = ''; } $self->_error( "Custom key '$key' must begin with 'x_' or 'X_'." ); return 0; } sub identifier { my ($self,$key) = @_; if(defined $key) { return 1 if($key && $key =~ /^([a-z][_a-z]+)$/i); # spec 2.0 defined } else { $key = ''; } $self->_error( "Key '$key' is not a legal identifier." ); return 0; } sub module { my ($self,$key) = @_; if(defined $key) { return 1 if($key && $key =~ /^[A-Za-z0-9_]+(::[A-Za-z0-9_]+)*$/); } else { $key = ''; } $self->_error( "Key '$key' is not a legal module name." ); return 0; } my @valid_phases = qw/ configure build test runtime develop /; sub phase { my ($self,$key) = @_; if(defined $key) { return 1 if( length $key && grep { $key eq $_ } @valid_phases ); return 1 if $key =~ /x_/i; } else { $key = ''; } $self->_error( "Key '$key' is not a legal phase." ); return 0; } my @valid_relations = qw/ requires recommends suggests conflicts /; sub relation { my ($self,$key) = @_; if(defined $key) { return 1 if( length $key && grep { $key eq $_ } @valid_relations ); return 1 if $key =~ /x_/i; } else { $key = ''; } $self->_error( "Key '$key' is not a legal prereq relationship." ); return 0; } sub _error { my $self = shift; my $mess = shift; $mess .= ' ('.join(' -> ',@{$self->{stack}}).')' if($self->{stack}); $mess .= " [Validation: $self->{spec}]"; push @{$self->{errors}}, $mess; } 1; # ABSTRACT: validate CPAN distribution metadata structures __END__ =pod =encoding UTF-8 =head1 NAME CPAN::Meta::Validator - validate CPAN distribution metadata structures =head1 VERSION version 2.143240 =head1 SYNOPSIS my $struct = decode_json_file('META.json'); my $cmv = CPAN::Meta::Validator->new( $struct ); unless ( $cmv->is_valid ) { my $msg = "Invalid META structure. Errors found:\n"; $msg .= join( "\n", $cmv->errors ); die $msg; } =head1 DESCRIPTION This module validates a CPAN Meta structure against the version of the the specification claimed in the C field of the structure. =head1 METHODS =head2 new my $cmv = CPAN::Meta::Validator->new( $struct ) The constructor must be passed a metadata structure. =head2 is_valid if ( $cmv->is_valid ) { ... } Returns a boolean value indicating whether the metadata provided is valid. =head2 errors warn( join "\n", $cmv->errors ); Returns a list of errors seen during validation. =begin :internals =head2 Check Methods =over =item * check_map($spec,$data) Checks whether a map (or hash) part of the data structure conforms to the appropriate specification definition. =item * check_list($spec,$data) Checks whether a list (or array) part of the data structure conforms to the appropriate specification definition. =item * =back =head2 Validator Methods =over =item * header($self,$key,$value) Validates that the header is valid. Note: No longer used as we now read the data structure, not the file. =item * url($self,$key,$value) Validates that a given value is in an acceptable URL format =item * urlspec($self,$key,$value) Validates that the URL to a META specification is a known one. =item * string_or_undef($self,$key,$value) Validates that the value is either a string or an undef value. Bit of a catchall function for parts of the data structure that are completely user defined. =item * string($self,$key,$value) Validates that a string exists for the given key. =item * file($self,$key,$value) Validate that a file is passed for the given key. This may be made more thorough in the future. For now it acts like \&string. =item * exversion($self,$key,$value) Validates a list of versions, e.g. '<= 5, >=2, ==3, !=4, >1, <6, 0'. =item * version($self,$key,$value) Validates a single version string. Versions of the type '5.8.8' and '0.00_00' are both valid. A leading 'v' like 'v1.2.3' is also valid. =item * boolean($self,$key,$value) Validates for a boolean value. Currently these values are '1', '0', 'true', 'false', however the latter 2 may be removed. =item * license($self,$key,$value) Validates that a value is given for the license. Returns 1 if an known license type, or 2 if a value is given but the license type is not a recommended one. =item * custom_1($self,$key,$value) Validates that the given key is in CamelCase, to indicate a user defined keyword and only has characters in the class [-_a-zA-Z]. In version 1.X of the spec, this was only explicitly stated for 'resources'. =item * custom_2($self,$key,$value) Validates that the given key begins with 'x_' or 'X_', to indicate a user defined keyword and only has characters in the class [-_a-zA-Z] =item * identifier($self,$key,$value) Validates that key is in an acceptable format for the META specification, for an identifier, i.e. any that matches the regular expression qr/[a-z][a-z_]/i. =item * module($self,$key,$value) Validates that a given key is in an acceptable module name format, e.g. 'Test::CPAN::Meta::Version'. =back =end :internals =for Pod::Coverage anything boolean check_list custom_1 custom_2 exversion file identifier license module phase relation release_status string string_or_undef url urlspec version header check_map =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut CPAN_META_VALIDATOR $fatpacked{"CPAN/Meta/YAML.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CPAN_META_YAML'; use 5.008001; # sane UTF-8 support use strict; use warnings; package CPAN::Meta::YAML; $CPAN::Meta::YAML::VERSION = '0.011'; BEGIN { $CPAN::Meta::YAML::AUTHORITY = 'cpan:ADAMK'; } # git description: v1.59-TRIAL-1-g33d9cd2 ; # original $VERSION removed by Doppelgaenger # XXX-INGY is 5.8.1 too old/broken for utf8? # XXX-XDG Lancaster consensus was that it was sufficient until # proven otherwise ##################################################################### # The CPAN::Meta::YAML API. # # These are the currently documented API functions/methods and # exports: use Exporter; our @ISA = qw{ Exporter }; our @EXPORT = qw{ Load Dump }; our @EXPORT_OK = qw{ LoadFile DumpFile freeze thaw }; ### # Functional/Export API: sub Dump { return CPAN::Meta::YAML->new(@_)->_dump_string; } # XXX-INGY Returning last document seems a bad behavior. # XXX-XDG I think first would seem more natural, but I don't know # that it's worth changing now sub Load { my $self = CPAN::Meta::YAML->_load_string(@_); if ( wantarray ) { return @$self; } else { # To match YAML.pm, return the last document return $self->[-1]; } } # XXX-INGY Do we really need freeze and thaw? # XXX-XDG I don't think so. I'd support deprecating them. BEGIN { *freeze = \&Dump; *thaw = \&Load; } sub DumpFile { my $file = shift; return CPAN::Meta::YAML->new(@_)->_dump_file($file); } sub LoadFile { my $file = shift; my $self = CPAN::Meta::YAML->_load_file($file); if ( wantarray ) { return @$self; } else { # Return only the last document to match YAML.pm, return $self->[-1]; } } ### # Object Oriented API: # Create an empty CPAN::Meta::YAML object # XXX-INGY Why do we use ARRAY object? # NOTE: I get it now, but I think it's confusing and not needed. # Will change it on a branch later, for review. # # XXX-XDG I don't support changing it yet. It's a very well-documented # "API" of CPAN::Meta::YAML. I'd support deprecating it, but Adam suggested # we not change it until YAML.pm's own OO API is established so that # users only have one API change to digest, not two sub new { my $class = shift; bless [ @_ ], $class; } # XXX-INGY It probably doesn't matter, and it's probably too late to # change, but 'read/write' are the wrong names. Read and Write # are actions that take data from storage to memory # characters/strings. These take the data to/from storage to native # Perl objects, which the terms dump and load are meant. As long as # this is a legacy quirk to CPAN::Meta::YAML it's ok, but I'd prefer not # to add new {read,write}_* methods to this API. sub read_string { my $self = shift; $self->_load_string(@_); } sub write_string { my $self = shift; $self->_dump_string(@_); } sub read { my $self = shift; $self->_load_file(@_); } sub write { my $self = shift; $self->_dump_file(@_); } ##################################################################### # Constants # Printed form of the unprintable characters in the lowest range # of ASCII characters, listed by ASCII ordinal position. my @UNPRINTABLE = qw( 0 x01 x02 x03 x04 x05 x06 a b t n v f r x0E x0F x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x1A e x1C x1D x1E x1F ); # Printable characters for escapes my %UNESCAPES = ( 0 => "\x00", z => "\x00", N => "\x85", a => "\x07", b => "\x08", t => "\x09", n => "\x0a", v => "\x0b", f => "\x0c", r => "\x0d", e => "\x1b", '\\' => '\\', ); # XXX-INGY # I(ngy) need to decide if these values should be quoted in # CPAN::Meta::YAML or not. Probably yes. # These 3 values have special meaning when unquoted and using the # default YAML schema. They need quotes if they are strings. my %QUOTE = map { $_ => 1 } qw{ null true false }; # The commented out form is simpler, but overloaded the Perl regex # engine due to recursion and backtracking problems on strings # larger than 32,000ish characters. Keep it for reference purposes. # qr/\"((?:\\.|[^\"])*)\"/ my $re_capture_double_quoted = qr/\"([^\\"]*(?:\\.[^\\"]*)*)\"/; my $re_capture_single_quoted = qr/\'([^\']*(?:\'\'[^\']*)*)\'/; # unquoted re gets trailing space that needs to be stripped my $re_capture_unquoted_key = qr/([^:]+(?::+\S[^:]*)*)(?=\s*\:(?:\s+|$))/; my $re_trailing_comment = qr/(?:\s+\#.*)?/; my $re_key_value_separator = qr/\s*:(?:\s+(?:\#.*)?|$)/; ##################################################################### # CPAN::Meta::YAML Implementation. # # These are the private methods that do all the work. They may change # at any time. ### # Loader functions: # Create an object from a file sub _load_file { my $class = ref $_[0] ? ref shift : shift; # Check the file my $file = shift or $class->_error( 'You did not specify a file name' ); $class->_error( "File '$file' does not exist" ) unless -e $file; $class->_error( "'$file' is a directory, not a file" ) unless -f _; $class->_error( "Insufficient permissions to read '$file'" ) unless -r _; # Open unbuffered with strict UTF-8 decoding and no translation layers open( my $fh, "<:unix:encoding(UTF-8)", $file ); unless ( $fh ) { $class->_error("Failed to open file '$file': $!"); } # flock if available (or warn if not possible for OS-specific reasons) if ( _can_flock() ) { flock( $fh, Fcntl::LOCK_SH() ) or warn "Couldn't lock '$file' for reading: $!"; } # slurp the contents my $contents = eval { use warnings FATAL => 'utf8'; local $/; <$fh> }; if ( my $err = $@ ) { $class->_error("Error reading from file '$file': $err"); } # close the file (release the lock) unless ( close $fh ) { $class->_error("Failed to close file '$file': $!"); } $class->_load_string( $contents ); } # Create an object from a string sub _load_string { my $class = ref $_[0] ? ref shift : shift; my $self = bless [], $class; my $string = $_[0]; eval { unless ( defined $string ) { die \"Did not provide a string to load"; } # Check if Perl has it marked as characters, but it's internally # inconsistent. E.g. maybe latin1 got read on a :utf8 layer if ( utf8::is_utf8($string) && ! utf8::valid($string) ) { die \<<'...'; Read an invalid UTF-8 string (maybe mixed UTF-8 and 8-bit character set). Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"? ... } # Ensure Unicode character semantics, even for 0x80-0xff utf8::upgrade($string); # Check for and strip any leading UTF-8 BOM $string =~ s/^\x{FEFF}//; # Check for some special cases return $self unless length $string; # Split the file into lines my @lines = grep { ! /^\s*(?:\#.*)?\z/ } split /(?:\015{1,2}\012|\015|\012)/, $string; # Strip the initial YAML header @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines; # A nibbling parser my $in_document = 0; while ( @lines ) { # Do we have a document header? if ( $lines[0] =~ /^---\s*(?:(.+)\s*)?\z/ ) { # Handle scalar documents shift @lines; if ( defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/ ) { push @$self, $self->_load_scalar( "$1", [ undef ], \@lines ); next; } $in_document = 1; } if ( ! @lines or $lines[0] =~ /^(?:---|\.\.\.)/ ) { # A naked document push @$self, undef; while ( @lines and $lines[0] !~ /^---/ ) { shift @lines; } $in_document = 0; # XXX The final '-+$' is to look for -- which ends up being an # error later. } elsif ( ! $in_document && @$self ) { # only the first document can be explicit die \"CPAN::Meta::YAML failed to classify the line '$lines[0]'"; } elsif ( $lines[0] =~ /^\s*\-(?:\s|$|-+$)/ ) { # An array at the root my $document = [ ]; push @$self, $document; $self->_load_array( $document, [ 0 ], \@lines ); } elsif ( $lines[0] =~ /^(\s*)\S/ ) { # A hash at the root my $document = { }; push @$self, $document; $self->_load_hash( $document, [ length($1) ], \@lines ); } else { # Shouldn't get here. @lines have whitespace-only lines # stripped, and previous match is a line with any # non-whitespace. So this clause should only be reachable via # a perlbug where \s is not symmetric with \S # uncoverable statement die \"CPAN::Meta::YAML failed to classify the line '$lines[0]'"; } } }; if ( ref $@ eq 'SCALAR' ) { $self->_error(${$@}); } elsif ( $@ ) { $self->_error($@); } return $self; } sub _unquote_single { my ($self, $string) = @_; return '' unless length $string; $string =~ s/\'\'/\'/g; return $string; } sub _unquote_double { my ($self, $string) = @_; return '' unless length $string; $string =~ s/\\"/"/g; $string =~ s{\\([Nnever\\fartz0b]|x([0-9a-fA-F]{2}))} {(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}}gex; return $string; } # Load a YAML scalar string to the actual Perl scalar sub _load_scalar { my ($self, $string, $indent, $lines) = @_; # Trim trailing whitespace $string =~ s/\s*\z//; # Explitic null/undef return undef if $string eq '~'; # Single quote if ( $string =~ /^$re_capture_single_quoted$re_trailing_comment\z/ ) { return $self->_unquote_single($1); } # Double quote. if ( $string =~ /^$re_capture_double_quoted$re_trailing_comment\z/ ) { return $self->_unquote_double($1); } # Special cases if ( $string =~ /^[\'\"!&]/ ) { die \"CPAN::Meta::YAML does not support a feature in line '$string'"; } return {} if $string =~ /^{}(?:\s+\#.*)?\z/; return [] if $string =~ /^\[\](?:\s+\#.*)?\z/; # Regular unquoted string if ( $string !~ /^[>|]/ ) { die \"CPAN::Meta::YAML found illegal characters in plain scalar: '$string'" if $string =~ /^(?:-(?:\s|$)|[\@\%\`])/ or $string =~ /:(?:\s|$)/; $string =~ s/\s+#.*\z//; return $string; } # Error die \"CPAN::Meta::YAML failed to find multi-line scalar content" unless @$lines; # Check the indent depth $lines->[0] =~ /^(\s*)/; $indent->[-1] = length("$1"); if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) { die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'"; } # Pull the lines my @multiline = (); while ( @$lines ) { $lines->[0] =~ /^(\s*)/; last unless length($1) >= $indent->[-1]; push @multiline, substr(shift(@$lines), length($1)); } my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n"; my $t = (substr($string, 1, 1) eq '-') ? '' : "\n"; return join( $j, @multiline ) . $t; } # Load an array sub _load_array { my ($self, $array, $indent, $lines) = @_; while ( @$lines ) { # Check for a new document if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) { while ( @$lines and $lines->[0] !~ /^---/ ) { shift @$lines; } return 1; } # Check the indent level $lines->[0] =~ /^(\s*)/; if ( length($1) < $indent->[-1] ) { return 1; } elsif ( length($1) > $indent->[-1] ) { die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'"; } if ( $lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/ ) { # Inline nested hash my $indent2 = length("$1"); $lines->[0] =~ s/-/ /; push @$array, { }; $self->_load_hash( $array->[-1], [ @$indent, $indent2 ], $lines ); } elsif ( $lines->[0] =~ /^\s*\-\s*\z/ ) { shift @$lines; unless ( @$lines ) { push @$array, undef; return 1; } if ( $lines->[0] =~ /^(\s*)\-/ ) { my $indent2 = length("$1"); if ( $indent->[-1] == $indent2 ) { # Null array entry push @$array, undef; } else { # Naked indenter push @$array, [ ]; $self->_load_array( $array->[-1], [ @$indent, $indent2 ], $lines ); } } elsif ( $lines->[0] =~ /^(\s*)\S/ ) { push @$array, { }; $self->_load_hash( $array->[-1], [ @$indent, length("$1") ], $lines ); } else { die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'"; } } elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/ ) { # Array entry with a value shift @$lines; push @$array, $self->_load_scalar( "$2", [ @$indent, undef ], $lines ); } elsif ( defined $indent->[-2] and $indent->[-1] == $indent->[-2] ) { # This is probably a structure like the following... # --- # foo: # - list # bar: value # # ... so lets return and let the hash parser handle it return 1; } else { die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'"; } } return 1; } # Load a hash sub _load_hash { my ($self, $hash, $indent, $lines) = @_; while ( @$lines ) { # Check for a new document if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) { while ( @$lines and $lines->[0] !~ /^---/ ) { shift @$lines; } return 1; } # Check the indent level $lines->[0] =~ /^(\s*)/; if ( length($1) < $indent->[-1] ) { return 1; } elsif ( length($1) > $indent->[-1] ) { die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'"; } # Find the key my $key; # Quoted keys if ( $lines->[0] =~ s/^\s*$re_capture_single_quoted$re_key_value_separator// ) { $key = $self->_unquote_single($1); } elsif ( $lines->[0] =~ s/^\s*$re_capture_double_quoted$re_key_value_separator// ) { $key = $self->_unquote_double($1); } elsif ( $lines->[0] =~ s/^\s*$re_capture_unquoted_key$re_key_value_separator// ) { $key = $1; $key =~ s/\s+$//; } elsif ( $lines->[0] =~ /^\s*\?/ ) { die \"CPAN::Meta::YAML does not support a feature in line '$lines->[0]'"; } else { die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'"; } # Do we have a value? if ( length $lines->[0] ) { # Yes $hash->{$key} = $self->_load_scalar( shift(@$lines), [ @$indent, undef ], $lines ); } else { # An indent shift @$lines; unless ( @$lines ) { $hash->{$key} = undef; return 1; } if ( $lines->[0] =~ /^(\s*)-/ ) { $hash->{$key} = []; $self->_load_array( $hash->{$key}, [ @$indent, length($1) ], $lines ); } elsif ( $lines->[0] =~ /^(\s*)./ ) { my $indent2 = length("$1"); if ( $indent->[-1] >= $indent2 ) { # Null hash entry $hash->{$key} = undef; } else { $hash->{$key} = {}; $self->_load_hash( $hash->{$key}, [ @$indent, length($1) ], $lines ); } } } } return 1; } ### # Dumper functions: # Save an object to a file sub _dump_file { my $self = shift; require Fcntl; # Check the file my $file = shift or $self->_error( 'You did not specify a file name' ); my $fh; # flock if available (or warn if not possible for OS-specific reasons) if ( _can_flock() ) { # Open without truncation (truncate comes after lock) my $flags = Fcntl::O_WRONLY()|Fcntl::O_CREAT(); sysopen( $fh, $file, $flags ); unless ( $fh ) { $self->_error("Failed to open file '$file' for writing: $!"); } # Use no translation and strict UTF-8 binmode( $fh, ":raw:encoding(UTF-8)"); flock( $fh, Fcntl::LOCK_EX() ) or warn "Couldn't lock '$file' for reading: $!"; # truncate and spew contents truncate $fh, 0; seek $fh, 0, 0; } else { open $fh, ">:unix:encoding(UTF-8)", $file; } # serialize and spew to the handle print {$fh} $self->_dump_string; # close the file (release the lock) unless ( close $fh ) { $self->_error("Failed to close file '$file': $!"); } return 1; } # Save an object to a string sub _dump_string { my $self = shift; return '' unless ref $self && @$self; # Iterate over the documents my $indent = 0; my @lines = (); eval { foreach my $cursor ( @$self ) { push @lines, '---'; # An empty document if ( ! defined $cursor ) { # Do nothing # A scalar document } elsif ( ! ref $cursor ) { $lines[-1] .= ' ' . $self->_dump_scalar( $cursor ); # A list at the root } elsif ( ref $cursor eq 'ARRAY' ) { unless ( @$cursor ) { $lines[-1] .= ' []'; next; } push @lines, $self->_dump_array( $cursor, $indent, {} ); # A hash at the root } elsif ( ref $cursor eq 'HASH' ) { unless ( %$cursor ) { $lines[-1] .= ' {}'; next; } push @lines, $self->_dump_hash( $cursor, $indent, {} ); } else { die \("Cannot serialize " . ref($cursor)); } } }; if ( ref $@ eq 'SCALAR' ) { $self->_error(${$@}); } elsif ( $@ ) { $self->_error($@); } join '', map { "$_\n" } @lines; } sub _has_internal_string_value { my $value = shift; my $b_obj = B::svref_2object(\$value); # for round trip problem return $b_obj->FLAGS & B::SVf_POK(); } sub _dump_scalar { my $string = $_[1]; my $is_key = $_[2]; # Check this before checking length or it winds up looking like a string! my $has_string_flag = _has_internal_string_value($string); return '~' unless defined $string; return "''" unless length $string; if (Scalar::Util::looks_like_number($string)) { # keys and values that have been used as strings get quoted if ( $is_key || $has_string_flag ) { return qq['$string']; } else { return $string; } } if ( $string =~ /[\x00-\x09\x0b-\x0d\x0e-\x1f\x7f-\x9f\'\n]/ ) { $string =~ s/\\/\\\\/g; $string =~ s/"/\\"/g; $string =~ s/\n/\\n/g; $string =~ s/[\x85]/\\N/g; $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g; $string =~ s/([\x7f-\x9f])/'\x' . sprintf("%X",ord($1))/ge; return qq|"$string"|; } if ( $string =~ /(?:^[~!@#%&*|>?:,'"`{}\[\]]|^-+$|\s|:\z)/ or $QUOTE{$string} ) { return "'$string'"; } return $string; } sub _dump_array { my ($self, $array, $indent, $seen) = @_; if ( $seen->{refaddr($array)}++ ) { die \"CPAN::Meta::YAML does not support circular references"; } my @lines = (); foreach my $el ( @$array ) { my $line = (' ' x $indent) . '-'; my $type = ref $el; if ( ! $type ) { $line .= ' ' . $self->_dump_scalar( $el ); push @lines, $line; } elsif ( $type eq 'ARRAY' ) { if ( @$el ) { push @lines, $line; push @lines, $self->_dump_array( $el, $indent + 1, $seen ); } else { $line .= ' []'; push @lines, $line; } } elsif ( $type eq 'HASH' ) { if ( keys %$el ) { push @lines, $line; push @lines, $self->_dump_hash( $el, $indent + 1, $seen ); } else { $line .= ' {}'; push @lines, $line; } } else { die \"CPAN::Meta::YAML does not support $type references"; } } @lines; } sub _dump_hash { my ($self, $hash, $indent, $seen) = @_; if ( $seen->{refaddr($hash)}++ ) { die \"CPAN::Meta::YAML does not support circular references"; } my @lines = (); foreach my $name ( sort keys %$hash ) { my $el = $hash->{$name}; my $line = (' ' x $indent) . $self->_dump_scalar($name, 1) . ":"; my $type = ref $el; if ( ! $type ) { $line .= ' ' . $self->_dump_scalar( $el ); push @lines, $line; } elsif ( $type eq 'ARRAY' ) { if ( @$el ) { push @lines, $line; push @lines, $self->_dump_array( $el, $indent + 1, $seen ); } else { $line .= ' []'; push @lines, $line; } } elsif ( $type eq 'HASH' ) { if ( keys %$el ) { push @lines, $line; push @lines, $self->_dump_hash( $el, $indent + 1, $seen ); } else { $line .= ' {}'; push @lines, $line; } } else { die \"CPAN::Meta::YAML does not support $type references"; } } @lines; } ##################################################################### # DEPRECATED API methods: # Error storage (DEPRECATED as of 1.57) our $errstr = ''; # Set error sub _error { require Carp; $errstr = $_[1]; $errstr =~ s/ at \S+ line \d+.*//; Carp::croak( $errstr ); } # Retrieve error my $errstr_warned; sub errstr { require Carp; Carp::carp( "CPAN::Meta::YAML->errstr and \$CPAN::Meta::YAML::errstr is deprecated" ) unless $errstr_warned++; $errstr; } ##################################################################### # Helper functions. Possibly not needed. # Use to detect nv or iv use B; # XXX-INGY Is flock CPAN::Meta::YAML's responsibility? # Some platforms can't flock :-( # XXX-XDG I think it is. When reading and writing files, we ought # to be locking whenever possible. People (foolishly) use YAML # files for things like session storage, which has race issues. my $HAS_FLOCK; sub _can_flock { if ( defined $HAS_FLOCK ) { return $HAS_FLOCK; } else { require Config; my $c = \%Config::Config; $HAS_FLOCK = grep { $c->{$_} } qw/d_flock d_fcntl_can_lock d_lockf/; require Fcntl if $HAS_FLOCK; return $HAS_FLOCK; } } # XXX-INGY Is this core in 5.8.1? Can we remove this? # XXX-XDG Scalar::Util 1.18 didn't land until 5.8.8, so we need this ##################################################################### # Use Scalar::Util if possible, otherwise emulate it BEGIN { local $@; if ( eval { require Scalar::Util } && $Scalar::Util::VERSION && eval($Scalar::Util::VERSION) >= 1.18 ) { *refaddr = *Scalar::Util::refaddr; } else { eval <<'END_PERL'; # Scalar::Util failed to load or too old sub refaddr { my $pkg = ref($_[0]) or return undef; if ( !! UNIVERSAL::can($_[0], 'can') ) { bless $_[0], 'Scalar::Util::Fake'; } else { $pkg = undef; } "$_[0]" =~ /0x(\w+)/; my $i = do { no warnings 'portable'; hex $1 }; bless $_[0], $pkg if defined $pkg; $i; } END_PERL } } 1; # XXX-INGY Doc notes I'm putting up here. Changing the doc when it's wrong # but leaving grey area stuff up here. # # I would like to change Read/Write to Load/Dump below without # changing the actual API names. # # It might be better to put Load/Dump API in the SYNOPSIS instead of the # dubious OO API. # # null and bool explanations may be outdated. =pod =encoding UTF-8 =head1 NAME CPAN::Meta::YAML - Read and write a subset of YAML for CPAN Meta files =head1 VERSION version 0.011 =head1 SYNOPSIS use CPAN::Meta::YAML; # reading a META file open $fh, "<:utf8", "META.yml"; $yaml_text = do { local $/; <$fh> }; $yaml = CPAN::Meta::YAML->read_string($yaml_text) or die CPAN::Meta::YAML->errstr; # finding the metadata $meta = $yaml->[0]; # writing a META file $yaml_text = $yaml->write_string or die CPAN::Meta::YAML->errstr; open $fh, ">:utf8", "META.yml"; print $fh $yaml_text; =head1 DESCRIPTION This module implements a subset of the YAML specification for use in reading and writing CPAN metadata files like F and F. It should not be used for any other general YAML parsing or generation task. NOTE: F (and F) files should be UTF-8 encoded. Users are responsible for proper encoding and decoding. In particular, the C and C methods do B support UTF-8 and should not be used. =head1 SUPPORT This module is currently derived from L by Adam Kennedy. If there are bugs in how it parses a particular META.yml file, please file a bug report in the YAML::Tiny bugtracker: L =head1 SEE ALSO L, L, L =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/dagolden/CPAN-Meta-YAML.git =head1 AUTHORS =over 4 =item * Adam Kennedy =item * David Golden =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by Adam Kennedy. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__ # ABSTRACT: Read and write a subset of YAML for CPAN Meta files CPAN_META_YAML $fatpacked{"Capture/Tiny.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CAPTURE_TINY'; use 5.006; use strict; use warnings; package Capture::Tiny; # ABSTRACT: Capture STDOUT and STDERR from Perl, XS or external programs our $VERSION = '0.48'; use Carp (); use Exporter (); use IO::Handle (); use File::Spec (); use File::Temp qw/tempfile tmpnam/; use Scalar::Util qw/reftype blessed/; # Get PerlIO or fake it BEGIN { local $@; eval { require PerlIO; PerlIO->can('get_layers') } or *PerlIO::get_layers = sub { return () }; } #--------------------------------------------------------------------------# # create API subroutines and export them # [do STDOUT flag, do STDERR flag, do merge flag, do tee flag] #--------------------------------------------------------------------------# my %api = ( capture => [1,1,0,0], capture_stdout => [1,0,0,0], capture_stderr => [0,1,0,0], capture_merged => [1,1,1,0], tee => [1,1,0,1], tee_stdout => [1,0,0,1], tee_stderr => [0,1,0,1], tee_merged => [1,1,1,1], ); for my $sub ( keys %api ) { my $args = join q{, }, @{$api{$sub}}; eval "sub $sub(&;@) {unshift \@_, $args; goto \\&_capture_tee;}"; ## no critic } our @ISA = qw/Exporter/; our @EXPORT_OK = keys %api; our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK ); #--------------------------------------------------------------------------# # constants and fixtures #--------------------------------------------------------------------------# my $IS_WIN32 = $^O eq 'MSWin32'; ##our $DEBUG = $ENV{PERL_CAPTURE_TINY_DEBUG}; ## ##my $DEBUGFH; ##open $DEBUGFH, "> DEBUG" if $DEBUG; ## ##*_debug = $DEBUG ? sub(@) { print {$DEBUGFH} @_ } : sub(){0}; our $TIMEOUT = 30; #--------------------------------------------------------------------------# # command to tee output -- the argument is a filename that must # be opened to signal that the process is ready to receive input. # This is annoying, but seems to be the best that can be done # as a simple, portable IPC technique #--------------------------------------------------------------------------# my @cmd = ($^X, '-C0', '-e', <<'HERE'); use Fcntl; $SIG{HUP}=sub{exit}; if ( my $fn=shift ) { sysopen(my $fh, qq{$fn}, O_WRONLY|O_CREAT|O_EXCL) or die $!; print {$fh} $$; close $fh; } my $buf; while (sysread(STDIN, $buf, 2048)) { syswrite(STDOUT, $buf); syswrite(STDERR, $buf); } HERE #--------------------------------------------------------------------------# # filehandle manipulation #--------------------------------------------------------------------------# sub _relayer { my ($fh, $apply_layers) = @_; # _debug("# requested layers (@{$layers}) for @{[fileno $fh]}\n"); # eliminate pseudo-layers binmode( $fh, ":raw" ); # strip off real layers until only :unix is left while ( 1 < ( my $layers =()= PerlIO::get_layers( $fh, output => 1 ) ) ) { binmode( $fh, ":pop" ); } # apply other layers my @to_apply = @$apply_layers; shift @to_apply; # eliminate initial :unix # _debug("# applying layers (unix @to_apply) to @{[fileno $fh]}\n"); binmode($fh, ":" . join(":",@to_apply)); } sub _name { my $glob = shift; no strict 'refs'; ## no critic return *{$glob}{NAME}; } sub _open { open $_[0], $_[1] or Carp::confess "Error from open(" . join(q{, }, @_) . "): $!"; # _debug( "# open " . join( ", " , map { defined $_ ? _name($_) : 'undef' } @_ ) . " as " . fileno( $_[0] ) . "\n" ); } sub _close { # _debug( "# closing " . ( defined $_[0] ? _name($_[0]) : 'undef' ) . " on " . fileno( $_[0] ) . "\n" ); close $_[0] or Carp::confess "Error from close(" . join(q{, }, @_) . "): $!"; } my %dup; # cache this so STDIN stays fd0 my %proxy_count; sub _proxy_std { my %proxies; if ( ! defined fileno STDIN ) { $proxy_count{stdin}++; if (defined $dup{stdin}) { _open \*STDIN, "<&=" . fileno($dup{stdin}); # _debug( "# restored proxy STDIN as " . (defined fileno STDIN ? fileno STDIN : 'undef' ) . "\n" ); } else { _open \*STDIN, "<" . File::Spec->devnull; # _debug( "# proxied STDIN as " . (defined fileno STDIN ? fileno STDIN : 'undef' ) . "\n" ); _open $dup{stdin} = IO::Handle->new, "<&=STDIN"; } $proxies{stdin} = \*STDIN; binmode(STDIN, ':utf8') if $] >= 5.008; ## no critic } if ( ! defined fileno STDOUT ) { $proxy_count{stdout}++; if (defined $dup{stdout}) { _open \*STDOUT, ">&=" . fileno($dup{stdout}); # _debug( "# restored proxy STDOUT as " . (defined fileno STDOUT ? fileno STDOUT : 'undef' ) . "\n" ); } else { _open \*STDOUT, ">" . File::Spec->devnull; # _debug( "# proxied STDOUT as " . (defined fileno STDOUT ? fileno STDOUT : 'undef' ) . "\n" ); _open $dup{stdout} = IO::Handle->new, ">&=STDOUT"; } $proxies{stdout} = \*STDOUT; binmode(STDOUT, ':utf8') if $] >= 5.008; ## no critic } if ( ! defined fileno STDERR ) { $proxy_count{stderr}++; if (defined $dup{stderr}) { _open \*STDERR, ">&=" . fileno($dup{stderr}); # _debug( "# restored proxy STDERR as " . (defined fileno STDERR ? fileno STDERR : 'undef' ) . "\n" ); } else { _open \*STDERR, ">" . File::Spec->devnull; # _debug( "# proxied STDERR as " . (defined fileno STDERR ? fileno STDERR : 'undef' ) . "\n" ); _open $dup{stderr} = IO::Handle->new, ">&=STDERR"; } $proxies{stderr} = \*STDERR; binmode(STDERR, ':utf8') if $] >= 5.008; ## no critic } return %proxies; } sub _unproxy { my (%proxies) = @_; # _debug( "# unproxying: " . join(" ", keys %proxies) . "\n" ); for my $p ( keys %proxies ) { $proxy_count{$p}--; # _debug( "# unproxied " . uc($p) . " ($proxy_count{$p} left)\n" ); if ( ! $proxy_count{$p} ) { _close $proxies{$p}; _close $dup{$p} unless $] < 5.008; # 5.6 will have already closed this as dup delete $dup{$p}; } } } sub _copy_std { my %handles; for my $h ( qw/stdout stderr stdin/ ) { next if $h eq 'stdin' && ! $IS_WIN32; # WIN32 hangs on tee without STDIN copied my $redir = $h eq 'stdin' ? "<&" : ">&"; _open $handles{$h} = IO::Handle->new(), $redir . uc($h); # ">&STDOUT" or "<&STDIN" } return \%handles; } # In some cases we open all (prior to forking) and in others we only open # the output handles (setting up redirection) sub _open_std { my ($handles) = @_; _open \*STDIN, "<&" . fileno $handles->{stdin} if defined $handles->{stdin}; _open \*STDOUT, ">&" . fileno $handles->{stdout} if defined $handles->{stdout}; _open \*STDERR, ">&" . fileno $handles->{stderr} if defined $handles->{stderr}; } #--------------------------------------------------------------------------# # private subs #--------------------------------------------------------------------------# sub _start_tee { my ($which, $stash) = @_; # $which is "stdout" or "stderr" # setup pipes $stash->{$_}{$which} = IO::Handle->new for qw/tee reader/; pipe $stash->{reader}{$which}, $stash->{tee}{$which}; # _debug( "# pipe for $which\: " . _name($stash->{tee}{$which}) . " " . fileno( $stash->{tee}{$which} ) . " => " . _name($stash->{reader}{$which}) . " " . fileno( $stash->{reader}{$which}) . "\n" ); select((select($stash->{tee}{$which}), $|=1)[0]); # autoflush # setup desired redirection for parent and child $stash->{new}{$which} = $stash->{tee}{$which}; $stash->{child}{$which} = { stdin => $stash->{reader}{$which}, stdout => $stash->{old}{$which}, stderr => $stash->{capture}{$which}, }; # flag file is used to signal the child is ready $stash->{flag_files}{$which} = scalar( tmpnam() ) . $$; # execute @cmd as a separate process if ( $IS_WIN32 ) { my $old_eval_err=$@; undef $@; eval "use Win32API::File qw/GetOsFHandle SetHandleInformation fileLastError HANDLE_FLAG_INHERIT INVALID_HANDLE_VALUE/ "; # _debug( "# Win32API::File loaded\n") unless $@; my $os_fhandle = GetOsFHandle( $stash->{tee}{$which} ); # _debug( "# Couldn't get OS handle: " . fileLastError() . "\n") if ! defined $os_fhandle || $os_fhandle == INVALID_HANDLE_VALUE(); my $result = SetHandleInformation( $os_fhandle, HANDLE_FLAG_INHERIT(), 0); # _debug( $result ? "# set no-inherit flag on $which tee\n" : ("# can't disable tee handle flag inherit: " . fileLastError() . "\n")); _open_std( $stash->{child}{$which} ); $stash->{pid}{$which} = system(1, @cmd, $stash->{flag_files}{$which}); # not restoring std here as it all gets redirected again shortly anyway $@=$old_eval_err; } else { # use fork _fork_exec( $which, $stash ); } } sub _fork_exec { my ($which, $stash) = @_; # $which is "stdout" or "stderr" my $pid = fork; if ( not defined $pid ) { Carp::confess "Couldn't fork(): $!"; } elsif ($pid == 0) { # child # _debug( "# in child process ...\n" ); untie *STDIN; untie *STDOUT; untie *STDERR; _close $stash->{tee}{$which}; # _debug( "# redirecting handles in child ...\n" ); _open_std( $stash->{child}{$which} ); # _debug( "# calling exec on command ...\n" ); exec @cmd, $stash->{flag_files}{$which}; } $stash->{pid}{$which} = $pid } my $have_usleep = eval "use Time::HiRes 'usleep'; 1"; sub _files_exist { return 1 if @_ == grep { -f } @_; Time::HiRes::usleep(1000) if $have_usleep; return 0; } sub _wait_for_tees { my ($stash) = @_; my $start = time; my @files = values %{$stash->{flag_files}}; my $timeout = defined $ENV{PERL_CAPTURE_TINY_TIMEOUT} ? $ENV{PERL_CAPTURE_TINY_TIMEOUT} : $TIMEOUT; 1 until _files_exist(@files) || ($timeout && (time - $start > $timeout)); Carp::confess "Timed out waiting for subprocesses to start" if ! _files_exist(@files); unlink $_ for @files; } sub _kill_tees { my ($stash) = @_; if ( $IS_WIN32 ) { # _debug( "# closing handles\n"); close($_) for values %{ $stash->{tee} }; # _debug( "# waiting for subprocesses to finish\n"); my $start = time; 1 until wait == -1 || (time - $start > 30); } else { _close $_ for values %{ $stash->{tee} }; waitpid $_, 0 for values %{ $stash->{pid} }; } } sub _slurp { my ($name, $stash) = @_; my ($fh, $pos) = map { $stash->{$_}{$name} } qw/capture pos/; # _debug( "# slurping captured $name from " . fileno($fh) . " at pos $pos with layers: @{[PerlIO::get_layers($fh)]}\n"); seek( $fh, $pos, 0 ) or die "Couldn't seek on capture handle for $name\n"; my $text = do { local $/; scalar readline $fh }; return defined($text) ? $text : ""; } #--------------------------------------------------------------------------# # _capture_tee() -- generic main sub for capturing or teeing #--------------------------------------------------------------------------# sub _capture_tee { # _debug( "# starting _capture_tee with (@_)...\n" ); my ($do_stdout, $do_stderr, $do_merge, $do_tee, $code, @opts) = @_; my %do = ($do_stdout ? (stdout => 1) : (), $do_stderr ? (stderr => 1) : ()); Carp::confess("Custom capture options must be given as key/value pairs\n") unless @opts % 2 == 0; my $stash = { capture => { @opts } }; for ( keys %{$stash->{capture}} ) { my $fh = $stash->{capture}{$_}; Carp::confess "Custom handle for $_ must be seekable\n" unless ref($fh) eq 'GLOB' || (blessed($fh) && $fh->isa("IO::Seekable")); } # save existing filehandles and setup captures local *CT_ORIG_STDIN = *STDIN ; local *CT_ORIG_STDOUT = *STDOUT; local *CT_ORIG_STDERR = *STDERR; # find initial layers my %layers = ( stdin => [PerlIO::get_layers(\*STDIN) ], stdout => [PerlIO::get_layers(\*STDOUT, output => 1)], stderr => [PerlIO::get_layers(\*STDERR, output => 1)], ); # _debug( "# existing layers for $_\: @{$layers{$_}}\n" ) for qw/stdin stdout stderr/; # get layers from underlying glob of tied filehandles if we can # (this only works for things that work like Tie::StdHandle) $layers{stdout} = [PerlIO::get_layers(tied *STDOUT)] if tied(*STDOUT) && (reftype tied *STDOUT eq 'GLOB'); $layers{stderr} = [PerlIO::get_layers(tied *STDERR)] if tied(*STDERR) && (reftype tied *STDERR eq 'GLOB'); # _debug( "# tied object corrected layers for $_\: @{$layers{$_}}\n" ) for qw/stdin stdout stderr/; # bypass scalar filehandles and tied handles # localize scalar STDIN to get a proxy to pick up FD0, then restore later to CT_ORIG_STDIN my %localize; $localize{stdin}++, local(*STDIN) if grep { $_ eq 'scalar' } @{$layers{stdin}}; $localize{stdout}++, local(*STDOUT) if $do_stdout && grep { $_ eq 'scalar' } @{$layers{stdout}}; $localize{stderr}++, local(*STDERR) if ($do_stderr || $do_merge) && grep { $_ eq 'scalar' } @{$layers{stderr}}; $localize{stdin}++, local(*STDIN), _open( \*STDIN, "<&=0") if tied *STDIN && $] >= 5.008; $localize{stdout}++, local(*STDOUT), _open( \*STDOUT, ">&=1") if $do_stdout && tied *STDOUT && $] >= 5.008; $localize{stderr}++, local(*STDERR), _open( \*STDERR, ">&=2") if ($do_stderr || $do_merge) && tied *STDERR && $] >= 5.008; # _debug( "# localized $_\n" ) for keys %localize; # proxy any closed/localized handles so we don't use fds 0, 1 or 2 my %proxy_std = _proxy_std(); # _debug( "# proxy std: @{ [%proxy_std] }\n" ); # update layers after any proxying $layers{stdout} = [PerlIO::get_layers(\*STDOUT, output => 1)] if $proxy_std{stdout}; $layers{stderr} = [PerlIO::get_layers(\*STDERR, output => 1)] if $proxy_std{stderr}; # _debug( "# post-proxy layers for $_\: @{$layers{$_}}\n" ) for qw/stdin stdout stderr/; # store old handles and setup handles for capture $stash->{old} = _copy_std(); $stash->{new} = { %{$stash->{old}} }; # default to originals for ( keys %do ) { $stash->{new}{$_} = ($stash->{capture}{$_} ||= File::Temp->new); seek( $stash->{capture}{$_}, 0, 2 ) or die "Could not seek on capture handle for $_\n"; $stash->{pos}{$_} = tell $stash->{capture}{$_}; # _debug("# will capture $_ on " . fileno($stash->{capture}{$_})."\n" ); _start_tee( $_ => $stash ) if $do_tee; # tees may change $stash->{new} } _wait_for_tees( $stash ) if $do_tee; # finalize redirection $stash->{new}{stderr} = $stash->{new}{stdout} if $do_merge; # _debug( "# redirecting in parent ...\n" ); _open_std( $stash->{new} ); # execute user provided code my ($exit_code, $inner_error, $outer_error, $orig_pid, @result); { $orig_pid = $$; local *STDIN = *CT_ORIG_STDIN if $localize{stdin}; # get original, not proxy STDIN # _debug( "# finalizing layers ...\n" ); _relayer(\*STDOUT, $layers{stdout}) if $do_stdout; _relayer(\*STDERR, $layers{stderr}) if $do_stderr; # _debug( "# running code $code ...\n" ); my $old_eval_err=$@; undef $@; eval { @result = $code->(); $inner_error = $@ }; $exit_code = $?; # save this for later $outer_error = $@; # save this for later STDOUT->flush if $do_stdout; STDERR->flush if $do_stderr; $@ = $old_eval_err; } # restore prior filehandles and shut down tees # _debug( "# restoring filehandles ...\n" ); _open_std( $stash->{old} ); _close( $_ ) for values %{$stash->{old}}; # don't leak fds # shouldn't need relayering originals, but see rt.perl.org #114404 _relayer(\*STDOUT, $layers{stdout}) if $do_stdout; _relayer(\*STDERR, $layers{stderr}) if $do_stderr; _unproxy( %proxy_std ); # _debug( "# killing tee subprocesses ...\n" ) if $do_tee; _kill_tees( $stash ) if $do_tee; # return captured output, but shortcut in void context # unless we have to echo output to tied/scalar handles; my %got; if ( $orig_pid == $$ and ( defined wantarray or ($do_tee && keys %localize) ) ) { for ( keys %do ) { _relayer($stash->{capture}{$_}, $layers{$_}); $got{$_} = _slurp($_, $stash); # _debug("# slurped " . length($got{$_}) . " bytes from $_\n"); } print CT_ORIG_STDOUT $got{stdout} if $do_stdout && $do_tee && $localize{stdout}; print CT_ORIG_STDERR $got{stderr} if $do_stderr && $do_tee && $localize{stderr}; } $? = $exit_code; $@ = $inner_error if $inner_error; die $outer_error if $outer_error; # _debug( "# ending _capture_tee with (@_)...\n" ); return unless defined wantarray; my @return; push @return, $got{stdout} if $do_stdout; push @return, $got{stderr} if $do_stderr && ! $do_merge; push @return, @result; return wantarray ? @return : $return[0]; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Capture::Tiny - Capture STDOUT and STDERR from Perl, XS or external programs =head1 VERSION version 0.48 =head1 SYNOPSIS use Capture::Tiny ':all'; # capture from external command ($stdout, $stderr, $exit) = capture { system( $cmd, @args ); }; # capture from arbitrary code (Perl or external) ($stdout, $stderr, @result) = capture { # your code here }; # capture partial or merged output $stdout = capture_stdout { ... }; $stderr = capture_stderr { ... }; $merged = capture_merged { ... }; # tee output ($stdout, $stderr) = tee { # your code here }; $stdout = tee_stdout { ... }; $stderr = tee_stderr { ... }; $merged = tee_merged { ... }; =head1 DESCRIPTION Capture::Tiny provides a simple, portable way to capture almost anything sent to STDOUT or STDERR, regardless of whether it comes from Perl, from XS code or from an external program. Optionally, output can be teed so that it is captured while being passed through to the original filehandles. Yes, it even works on Windows (usually). Stop guessing which of a dozen capturing modules to use in any particular situation and just use this one. =head1 USAGE The following functions are available. None are exported by default. =head2 capture ($stdout, $stderr, @result) = capture \&code; $stdout = capture \&code; The C function takes a code reference and returns what is sent to STDOUT and STDERR as well as any return values from the code reference. In scalar context, it returns only STDOUT. If no output was received for a filehandle, it returns an empty string for that filehandle. Regardless of calling context, all output is captured -- nothing is passed to the existing filehandles. It is prototyped to take a subroutine reference as an argument. Thus, it can be called in block form: ($stdout, $stderr) = capture { # your code here ... }; Note that the coderef is evaluated in list context. If you wish to force scalar context on the return value, you must use the C keyword. ($stdout, $stderr, $count) = capture { my @list = qw/one two three/; return scalar @list; # $count will be 3 }; Also note that within the coderef, the C<@_> variable will be empty. So don't use arguments from a surrounding subroutine without copying them to an array first: sub wont_work { my ($stdout, $stderr) = capture { do_stuff( @_ ) }; # WRONG ... } sub will_work { my @args = @_; my ($stdout, $stderr) = capture { do_stuff( @args ) }; # RIGHT ... } Captures are normally done to an anonymous temporary filehandle. To capture via a named file (e.g. to externally monitor a long-running capture), provide custom filehandles as a trailing list of option pairs: my $out_fh = IO::File->new("out.txt", "w+"); my $err_fh = IO::File->new("out.txt", "w+"); capture { ... } stdout => $out_fh, stderr => $err_fh; The filehandles must be read/write and seekable. Modifying the files or filehandles during a capture operation will give unpredictable results. Existing IO layers on them may be changed by the capture. When called in void context, C saves memory and time by not reading back from the capture handles. =head2 capture_stdout ($stdout, @result) = capture_stdout \&code; $stdout = capture_stdout \&code; The C function works just like C except only STDOUT is captured. STDERR is not captured. =head2 capture_stderr ($stderr, @result) = capture_stderr \&code; $stderr = capture_stderr \&code; The C function works just like C except only STDERR is captured. STDOUT is not captured. =head2 capture_merged ($merged, @result) = capture_merged \&code; $merged = capture_merged \&code; The C function works just like C except STDOUT and STDERR are merged. (Technically, STDERR is redirected to the same capturing handle as STDOUT before executing the function.) Caution: STDOUT and STDERR output in the merged result are not guaranteed to be properly ordered due to buffering. =head2 tee ($stdout, $stderr, @result) = tee \&code; $stdout = tee \&code; The C function works just like C, except that output is captured as well as passed on to the original STDOUT and STDERR. When called in void context, C saves memory and time by not reading back from the capture handles, except when the original STDOUT OR STDERR were tied or opened to a scalar handle. =head2 tee_stdout ($stdout, @result) = tee_stdout \&code; $stdout = tee_stdout \&code; The C function works just like C except only STDOUT is teed. STDERR is not teed (output goes to STDERR as usual). =head2 tee_stderr ($stderr, @result) = tee_stderr \&code; $stderr = tee_stderr \&code; The C function works just like C except only STDERR is teed. STDOUT is not teed (output goes to STDOUT as usual). =head2 tee_merged ($merged, @result) = tee_merged \&code; $merged = tee_merged \&code; The C function works just like C except that output is captured as well as passed on to STDOUT. Caution: STDOUT and STDERR output in the merged result are not guaranteed to be properly ordered due to buffering. =head1 LIMITATIONS =head2 Portability Portability is a goal, not a guarantee. C requires fork, except on Windows where C is used instead. Not tested on any particularly esoteric platforms yet. See the L for test result by platform. =head2 PerlIO layers Capture::Tiny does its best to preserve PerlIO layers such as ':utf8' or ':crlf' when capturing (only for Perl 5.8.1+) . Layers should be applied to STDOUT or STDERR I the call to C or C. This may not work for tied filehandles (see below). =head2 Modifying filehandles before capturing Generally speaking, you should do little or no manipulation of the standard IO filehandles prior to using Capture::Tiny. In particular, closing, reopening, localizing or tying standard filehandles prior to capture may cause a variety of unexpected, undesirable and/or unreliable behaviors, as described below. Capture::Tiny does its best to compensate for these situations, but the results may not be what you desire. =head3 Closed filehandles Capture::Tiny will work even if STDIN, STDOUT or STDERR have been previously closed. However, since they will be reopened to capture or tee output, any code within the captured block that depends on finding them closed will, of course, not find them to be closed. If they started closed, Capture::Tiny will close them again when the capture block finishes. Note that this reopening will happen even for STDIN or a filehandle not being captured to ensure that the filehandle used for capture is not opened to file descriptor 0, as this causes problems on various platforms. Prior to Perl 5.12, closed STDIN combined with PERL_UNICODE=D leaks filehandles and also breaks tee() for undiagnosed reasons. So don't do that. =head3 Localized filehandles If code localizes any of Perl's standard filehandles before capturing, the capture will affect the localized filehandles and not the original ones. External system calls are not affected by localizing a filehandle in Perl and will continue to send output to the original filehandles (which will thus not be captured). =head3 Scalar filehandles If STDOUT or STDERR are reopened to scalar filehandles prior to the call to C or C, then Capture::Tiny will override the output filehandle for the duration of the C or C call and then, for C, send captured output to the output filehandle after the capture is complete. (Requires Perl 5.8) Capture::Tiny attempts to preserve the semantics of STDIN opened to a scalar reference, but note that external processes will not be able to read from such a handle. Capture::Tiny tries to ensure that external processes will read from the null device instead, but this is not guaranteed. =head3 Tied output filehandles If STDOUT or STDERR are tied prior to the call to C or C, then Capture::Tiny will attempt to override the tie for the duration of the C or C call and then send captured output to the tied filehandle after the capture is complete. (Requires Perl 5.8) Capture::Tiny may not succeed resending UTF-8 encoded data to a tied STDOUT or STDERR filehandle. Characters may appear as bytes. If the tied filehandle is based on L, then Capture::Tiny will attempt to determine appropriate layers like C<:utf8> from the underlying filehandle and do the right thing. =head3 Tied input filehandle Capture::Tiny attempts to preserve the semantics of tied STDIN, but this requires Perl 5.8 and is not entirely predictable. External processes will not be able to read from such a handle. Unless having STDIN tied is crucial, it may be safest to localize STDIN when capturing: my ($out, $err) = do { local *STDIN; capture { ... } }; =head2 Modifying filehandles during a capture Attempting to modify STDIN, STDOUT or STDERR I C or C is almost certainly going to cause problems. Don't do that. =head3 Forking inside a capture Forks aren't portable. The behavior of filehandles during a fork is even less so. If Capture::Tiny detects that a fork has occurred within a capture, it will shortcut in the child process and return empty strings for captures. Other problems may occur in the child or parent, as well. Forking in a capture block is not recommended. =head3 Using threads Filehandles are global. Mixing up I/O and captures in different threads without coordination is going to cause problems. Besides, threads are officially discouraged. =head3 Dropping privileges during a capture If you drop privileges during a capture, temporary files created to facilitate the capture may not be cleaned up afterwards. =head2 No support for Perl 5.8.0 It's just too buggy when it comes to layers and UTF-8. Perl 5.8.1 or later is recommended. =head2 Limited support for Perl 5.6 Perl 5.6 predates PerlIO. UTF-8 data may not be captured correctly. =head1 ENVIRONMENT =head2 PERL_CAPTURE_TINY_TIMEOUT Capture::Tiny uses subprocesses internally for C. By default, Capture::Tiny will timeout with an error if such subprocesses are not ready to receive data within 30 seconds (or whatever is the value of C<$Capture::Tiny::TIMEOUT>). An alternate timeout may be specified by setting the C environment variable. Setting it to zero will disable timeouts. B, this does not timeout the code reference being captured -- this only prevents Capture::Tiny itself from hanging your process waiting for its child processes to be ready to proceed. =head1 SEE ALSO This module was inspired by L, which provides similar functionality without the ability to tee output and with more complicated code and API. L does not handle layers or most of the unusual cases described in the L section and I no longer recommend it. There are many other CPAN modules that provide some sort of output capture, albeit with various limitations that make them appropriate only in particular circumstances. I'm probably missing some. The long list is provided to show why I felt Capture::Tiny was necessary. =over 4 =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =item * L =back =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/dagolden/Capture-Tiny.git =head1 AUTHOR David Golden =head1 CONTRIBUTORS =for stopwords Dagfinn Ilmari Mannsåker David E. Wheeler fecundf Graham Knop Peter Rabbitson =over 4 =item * Dagfinn Ilmari Mannsåker =item * David E. Wheeler =item * fecundf =item * Graham Knop =item * Peter Rabbitson =back =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2009 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut CAPTURE_TINY $fatpacked{"Carton.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON'; package Carton; use strict; use 5.008_005; use version; our $VERSION = version->declare("v1.0.34"); 1; __END__ =head1 NAME Carton - Perl module dependency manager (aka Bundler for Perl) =head1 SYNOPSIS # On your development environment > cat cpanfile requires 'Plack', '0.9980'; requires 'Starman', '0.2000'; > carton install > git add cpanfile cpanfile.snapshot > git commit -m "add Plack and Starman" # Other developer's machine, or on a deployment box > carton install > carton exec starman -p 8080 myapp.psgi # carton exec is optional > perl -Ilocal/lib/perl5 local/bin/starman -p 8080 myapp.psgi > PERL5LIB=/path/to/local/lib/perl5 /path/to/local/bin/starman -p 8080 myapp.psgi =head1 AVAILABILITY Carton only works with perl installation with the complete set of core modules. If you use perl installed by a vendor package with modules stripped from core, Carton is not expected to work correctly. Also, Carton requires you to run your command/application with C command or to include the I directory in your Perl library search path (using C, C<-I>, or L). =head1 DESCRIPTION carton is a command line tool to track the Perl module dependencies for your Perl application. Dependencies are declared using L format, and the managed dependencies are tracked in a I file, which is meant to be version controlled, and the snapshot file allows other developers of your application will have the exact same versions of the modules. For C syntax, see L documentation. =head1 TUTORIAL =head2 Initializing the environment carton will use the I directory to install modules into. You're recommended to exclude these directories from the version control system. > echo local/ >> .gitignore > git add cpanfile cpanfile.snapshot > git commit -m "Start using carton" =head2 Tracking the dependencies You can manage the dependencies of your application via C. # cpanfile requires 'Plack', '0.9980'; requires 'Starman', '0.2000'; And then you can install these dependencies via: > carton install The modules are installed into your I directory, and the dependencies tree and version information are analyzed and saved into I in your directory. Make sure you add I and I to your version controlled repository and commit changes as you update dependencies. This will ensure that other developers on your app, as well as your deployment environment, use exactly the same versions of the modules you just installed. > git add cpanfile cpanfile.snapshot > git commit -m "Added Plack and Starman" =head2 Specifying a CPAN distribution You can pin a module resolution to a specific distribution using a combination of C, C and C options in C. # specific distribution on PAUSE requires 'Plack', '== 0.9980', dist => 'MIYAGAWA/Plack-0.9980.tar.gz'; # local mirror (darkpan) requires 'Plack', '== 0.9981', dist => 'MYCOMPANY/Plack-0.9981-p1.tar.gz', mirror => 'https://pause.local/'; # URL requires 'Plack', '== 1.1000', url => 'https://pause.local/authors/id/M/MY/MYCOMPANY/Plack-1.1000.tar.gz'; =head2 Deploying your application Once you've done installing all the dependencies, you can push your application directory to a remote machine (excluding I and I<.carton>) and run the following command: > carton install --deployment This will look at the I and install the exact same versions of the dependencies into I, and now your application is ready to run. The C<--deployment> flag makes sure that carton will only install modules and versions available in your snapshot, and won't fallback to query for CPAN Meta DB for missing modules. =head2 Bundling modules carton can bundle all the tarballs for your dependencies into a directory so that you can even install dependencies that are not available on CPAN, such as internal distribution aka DarkPAN. > carton bundle will bundle these tarballs into I directory, and > carton install --cached will install modules using this local cache. Combined with C<--deployment> option, you can avoid querying for a database like CPAN Meta DB or downloading files from CPAN mirrors upon deployment time. As of Carton v1.0.32, the bundle also includes a package index allowing you to simply use L (which has a L) instead of installing Carton on a remote machine. > cpanm -L local --from "$PWD/vendor/cache" --installdeps --notest --quiet . =head1 PERL VERSIONS When you take a snapshot in one perl version and deploy on another (different) version, you might have troubles with core modules. The simplest solution, which might not work for everybody, is to use the same version of perl in the development and deployment. To enforce that, you're recommended to use L and C<.perl-version> to lock perl versions in development. You can also specify the minimum perl required in C: requires 'perl', '5.16.3'; and carton (and cpanm) will give you errors when deployed on hosts with perl lower than the specified version. =head1 COMMUNITY =over 4 =item L Code repository, Wiki and Issue Tracker =item L IRC chat room =back =head1 AUTHOR Tatsuhiko Miyagawa =head1 COPYRIGHT Tatsuhiko Miyagawa 2011- =head1 LICENSE This software is licensed under the same terms as Perl itself. =head1 SEE ALSO L L L L L L L =cut CARTON $fatpacked{"Carton/Builder.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_BUILDER'; package Carton::Builder; use strict; use Class::Tiny { mirror => undef, index => undef, cascade => sub { 1 }, without => sub { [] }, cpanfile => undef, }; sub effective_mirrors { my $self = shift; # push default CPAN mirror always, as a fallback # TODO don't pass fallback if --cached is set? my @mirrors = ($self->mirror); push @mirrors, Carton::Mirror->default if $self->custom_mirror; push @mirrors, Carton::Mirror->new('http://backpan.perl.org/'); @mirrors; } sub custom_mirror { my $self = shift; ! $self->mirror->is_default; } sub bundle { my($self, $path, $cache_path, $snapshot) = @_; for my $dist ($snapshot->distributions) { my $source = $path->child("cache/authors/id/" . $dist->pathname); my $target = $cache_path->child("authors/id/" . $dist->pathname); if ($source->exists) { warn "Copying ", $dist->pathname, "\n"; $target->parent->mkpath; $source->copy($target) or warn "$target: $!"; } else { warn "Couldn't find @{[ $dist->pathname ]}\n"; } } my $has_io_gzip = eval { require IO::Compress::Gzip; 1 }; my $ext = $has_io_gzip ? ".txt.gz" : ".txt"; my $index = $cache_path->child("modules/02packages.details$ext"); $index->parent->mkpath; warn "Writing $index\n"; my $out = $index->openw; if ($has_io_gzip) { $out = IO::Compress::Gzip->new($out) or die "gzip failed: $IO::Compress::Gzip::GzipError"; } $snapshot->index->write($out); close $out; unless ($has_io_gzip) { unlink "$index.gz"; !system 'gzip', $index or die "Running gzip command failed: $!"; } } sub install { my($self, $path) = @_; $self->run_install( "-L", $path, (map { ("--mirror", $_->url) } $self->effective_mirrors), ( $self->index ? ("--mirror-index", $self->index) : () ), ( $self->cascade ? "--cascade-search" : () ), ( $self->custom_mirror ? "--mirror-only" : () ), "--save-dists", "$path/cache", $self->groups, "--cpanfile", $self->cpanfile, "--installdeps", $self->cpanfile->dirname, ) or die "Installing modules failed\n"; } sub groups { my $self = shift; # TODO support --without test (don't need test on deployment) my @options = ('--with-all-features', '--with-develop'); for my $group (@{$self->without}) { push @options, '--without-develop' if $group eq 'develop'; push @options, "--without-feature=$group"; } return @options; } sub update { my($self, $path, @modules) = @_; $self->run_install( "-L", $path, (map { ("--mirror", $_->url) } $self->effective_mirrors), ( $self->custom_mirror ? "--mirror-only" : () ), "--save-dists", "$path/cache", @modules ) or die "Updating modules failed\n"; } sub run_install { my($self, @args) = @_; require Menlo::CLI::Compat; local $ENV{PERL_CPANM_OPT}; my $cli = Menlo::CLI::Compat->new; $cli->parse_options("--quiet", "--notest", @args); $cli->run; !$cli->status; } 1; CARTON_BUILDER $fatpacked{"Carton/CLI.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_CLI'; package Carton::CLI; use strict; use warnings; use Config; use Getopt::Long; use Path::Tiny; use Try::Tiny; use Module::CoreList; use Scalar::Util qw(blessed); use Carton; use Carton::Builder; use Carton::Mirror; use Carton::Snapshot; use Carton::Util; use Carton::Environment; use Carton::Error; use constant { SUCCESS => 0, INFO => 1, WARN => 2, ERROR => 3 }; our $UseSystem = 0; # 1 for unit testing use Class::Tiny { verbose => undef, carton => sub { $_[0]->_build_carton }, mirror => sub { $_[0]->_build_mirror }, }; sub _build_mirror { my $self = shift; Carton::Mirror->new($ENV{PERL_CARTON_MIRROR} || $Carton::Mirror::DefaultMirror); } sub run { my($self, @args) = @_; my @commands; my $p = Getopt::Long::Parser->new( config => [ "no_ignore_case", "pass_through" ], ); $p->getoptionsfromarray( \@args, "h|help" => sub { unshift @commands, 'help' }, "v|version" => sub { unshift @commands, 'version' }, "verbose!" => sub { $self->verbose($_[1]) }, ); push @commands, @args; my $cmd = shift @commands || 'install'; my $code = try { my $call = $self->can("cmd_$cmd") or Carton::Error::CommandNotFound->throw(error => "Could not find command '$cmd'"); $self->$call(@commands); return 0; } catch { die $_ unless blessed $_ && $_->can('rethrow'); if ($_->isa('Carton::Error::CommandExit')) { return $_->code || 255; } elsif ($_->isa('Carton::Error::CommandNotFound')) { warn $_->error, "\n\n"; $self->cmd_usage; return 255; } elsif ($_->isa('Carton::Error')) { warn $_->error, "\n"; return 255; } }; return $code; } sub commands { my $self = shift; no strict 'refs'; map { s/^cmd_//; $_ } grep { /^cmd_.*/ && $self->can($_) } sort keys %{__PACKAGE__."::"}; } sub cmd_usage { my $self = shift; $self->print(< where is one of: @{[ join ", ", $self->commands ]} Run carton -h for help. HELP } sub parse_options { my($self, $args, @spec) = @_; my $p = Getopt::Long::Parser->new( config => [ "no_auto_abbrev", "no_ignore_case" ], ); $p->getoptionsfromarray($args, @spec); } sub parse_options_pass_through { my($self, $args, @spec) = @_; my $p = Getopt::Long::Parser->new( config => [ "no_auto_abbrev", "no_ignore_case", "pass_through" ], ); $p->getoptionsfromarray($args, @spec); # with pass_through keeps -- in args shift @$args if $args->[0] && $args->[0] eq '--'; } sub printf { my $self = shift; my $type = pop; my($temp, @args) = @_; $self->print(sprintf($temp, @args), $type); } sub print { my($self, $msg, $type) = @_; my $fh = $type && $type >= WARN ? *STDERR : *STDOUT; print {$fh} $msg; } sub error { my($self, $msg) = @_; $self->print($msg, ERROR); Carton::Error::CommandExit->throw; } sub cmd_help { my $self = shift; my $module = $_[0] ? ("Carton::Doc::" . ucfirst $_[0]) : "Carton.pm"; system "perldoc", $module; } sub cmd_version { my $self = shift; $self->print("carton $Carton::VERSION\n"); } sub cmd_bundle { my($self, @args) = @_; my $env = Carton::Environment->build; $env->snapshot->load; $self->print("Bundling modules using @{[$env->cpanfile]}\n"); my $builder = Carton::Builder->new( mirror => $self->mirror, cpanfile => $env->cpanfile, ); $builder->bundle($env->install_path, $env->vendor_cache, $env->snapshot); $self->printf("Complete! Modules were bundled into %s\n", $env->vendor_cache, SUCCESS); } sub cmd_fatpack { my($self, @args) = @_; my $env = Carton::Environment->build; require Carton::Packer; Carton::Packer->new->fatpack_carton($env->vendor_bin); } sub cmd_install { my($self, @args) = @_; my($install_path, $cpanfile_path, @without); $self->parse_options( \@args, "p|path=s" => \$install_path, "cpanfile=s" => \$cpanfile_path, "without=s" => sub { push @without, split /,/, $_[1] }, "deployment!" => \my $deployment, "cached!" => \my $cached, ); my $env = Carton::Environment->build($cpanfile_path, $install_path); $env->snapshot->load_if_exists; if ($deployment && !$env->snapshot->loaded) { $self->error("--deployment requires cpanfile.snapshot: Run `carton install` and make sure cpanfile.snapshot is checked into your version control.\n"); } my $builder = Carton::Builder->new( cascade => 1, mirror => $self->mirror, without => \@without, cpanfile => $env->cpanfile, ); # TODO: --without with no .lock won't fetch the groups, resulting in insufficient requirements if ($deployment) { $self->print("Installing modules using @{[$env->cpanfile]} (deployment mode)\n"); $builder->cascade(0); } else { $self->print("Installing modules using @{[$env->cpanfile]}\n"); } # TODO merge CPANfile git to mirror even if lock doesn't exist if ($env->snapshot->loaded) { my $index_file = $env->install_path->child("cache/modules/02packages.details.txt"); $index_file->parent->mkpath; $env->snapshot->write_index($index_file); $builder->index($index_file); } if ($cached) { $builder->mirror(Carton::Mirror->new($env->vendor_cache)); } $builder->install($env->install_path); unless ($deployment) { $env->cpanfile->load; $env->snapshot->find_installs($env->install_path, $env->cpanfile->requirements); $env->snapshot->save; } $self->print("Complete! Modules were installed into @{[$env->install_path]}\n", SUCCESS); } sub cmd_show { my($self, @args) = @_; my $env = Carton::Environment->build; $env->snapshot->load; for my $module (@args) { my $dist = $env->snapshot->find($module) or $self->error("Couldn't locate $module in cpanfile.snapshot\n"); $self->print( $dist->name . "\n" ); } } sub cmd_list { my($self, @args) = @_; my $format = 'name'; $self->parse_options( \@args, "distfile" => sub { $format = 'distfile' }, ); my $env = Carton::Environment->build; $env->snapshot->load; for my $dist ($env->snapshot->distributions) { $self->print($dist->$format . "\n"); } } sub cmd_tree { my($self, @args) = @_; my $env = Carton::Environment->build; $env->snapshot->load; $env->cpanfile->load; my %seen; my $dumper = sub { my($dependency, $reqs, $level) = @_; return if $level == 0; return Carton::Tree::STOP if $dependency->dist->is_core; return Carton::Tree::STOP if $seen{$dependency->distname}++; $self->printf( "%s%s (%s)\n", " " x ($level - 1), $dependency->module, $dependency->distname, INFO ); }; $env->tree->walk_down($dumper); } sub cmd_check { my($self, @args) = @_; my $cpanfile_path; $self->parse_options( \@args, "cpanfile=s" => \$cpanfile_path, ); my $env = Carton::Environment->build($cpanfile_path); $env->snapshot->load; $env->cpanfile->load; # TODO remove snapshot # TODO pass git spec to Requirements? my $merged_reqs = $env->tree->merged_requirements; my @missing; for my $module ($merged_reqs->required_modules) { my $install = $env->snapshot->find_or_core($module); if ($install) { unless ($merged_reqs->accepts_module($module => $install->version_for($module))) { push @missing, [ $module, 1, $install->version_for($module) ]; } } else { push @missing, [ $module, 0 ]; } } if (@missing) { $self->print("Following dependencies are not satisfied.\n", INFO); for my $missing (@missing) { my($module, $unsatisfied, $version) = @$missing; if ($unsatisfied) { $self->printf(" %s has version %s. Needs %s\n", $module, $version, $merged_reqs->requirements_for_module($module), INFO); } else { $self->printf(" %s is not installed. Needs %s\n", $module, $merged_reqs->requirements_for_module($module), INFO); } } $self->printf("Run `carton install` to install them.\n", INFO); Carton::Error::CommandExit->throw; } else { $self->print("cpanfile's dependencies are satisfied.\n", INFO); } } sub cmd_update { my($self, @args) = @_; my $env = Carton::Environment->build; $env->cpanfile->load; my $cpanfile = Module::CPANfile->load($env->cpanfile); @args = grep { $_ ne 'perl' } $env->cpanfile->required_modules unless @args; $env->snapshot->load; my @modules; for my $module (@args) { my $dist = $env->snapshot->find_or_core($module) or $self->error("Could not find module $module.\n"); next if $dist->is_core; push @modules, "$module~" . $env->cpanfile->requirements_for_module($module); } return unless @modules; my $builder = Carton::Builder->new( mirror => $self->mirror, cpanfile => $env->cpanfile, ); $builder->update($env->install_path, @modules); $env->snapshot->find_installs($env->install_path, $env->cpanfile->requirements); $env->snapshot->save; } sub cmd_run { my($self, @args) = @_; local $UseSystem = 1; $self->cmd_exec(@args); } sub cmd_exec { my($self, @args) = @_; my $env = Carton::Environment->build; $env->snapshot->load; # allows -Ilib @args = map { /^(-[I])(.+)/ ? ($1,$2) : $_ } @args; while (@args) { if ($args[0] eq '-I') { warn "exec -Ilib is deprecated. You might want to run: carton exec perl -Ilib ...\n"; splice(@args, 0, 2); } else { last; } } $self->parse_options_pass_through(\@args); # to handle -- unless (@args) { $self->error("carton exec needs a command to run.\n"); } # PERL5LIB takes care of arch my $path = $env->install_path; local $ENV{PERL5LIB} = "$path/lib/perl5"; local $ENV{PATH} = "$path/bin:$ENV{PATH}"; if ($UseSystem) { system @args; } else { exec @args; exit 127; # command not found } } 1; CARTON_CLI $fatpacked{"Carton/CPANfile.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_CPANFILE'; package Carton::CPANfile; use Path::Tiny (); use Module::CPANfile; use overload q{""} => sub { $_[0]->stringify }, fallback => 1; use subs 'path'; use Class::Tiny { path => undef, _cpanfile => undef, requirements => sub { $_[0]->_build_requirements }, }; sub stringify { shift->path->stringify(@_) } sub dirname { shift->path->dirname(@_) } sub prereqs { shift->_cpanfile->prereqs(@_) } sub required_modules { shift->requirements->required_modules(@_) } sub requirements_for_module { shift->requirements->requirements_for_module(@_) } sub path { my $self = shift; if (@_) { $self->{path} = Path::Tiny->new($_[0]); } else { $self->{path}; } } sub load { my $self = shift; $self->_cpanfile( Module::CPANfile->load($self->path) ); } sub _build_requirements { my $self = shift; my $reqs = CPAN::Meta::Requirements->new; $reqs->add_requirements($self->prereqs->requirements_for($_, 'requires')) for qw( configure build runtime test develop ); $reqs->clear_requirement('perl'); $reqs; } 1; CARTON_CPANFILE $fatpacked{"Carton/Dependency.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_DEPENDENCY'; package Carton::Dependency; use strict; use Class::Tiny { module => undef, requirement => undef, dist => undef, }; sub requirements { shift->dist->requirements(@_) } sub distname { my $self = shift; $self->dist->name; } sub version { my $self = shift; $self->dist->version_for($self->module); } 1; CARTON_DEPENDENCY $fatpacked{"Carton/Dist.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_DIST'; package Carton::Dist; use strict; use Class::Tiny { name => undef, pathname => undef, provides => sub { +{} }, requirements => sub { $_[0]->_build_requirements }, }; use CPAN::Meta; sub add_string_requirement { shift->requirements->add_string_requirement(@_) } sub required_modules { shift->requirements->required_modules(@_) } sub requirements_for_module { shift->requirements->requirements_for_module(@_) } sub is_core { 0 } sub distfile { my $self = shift; $self->pathname; } sub _build_requirements { CPAN::Meta::Requirements->new; } sub provides_module { my($self, $module) = @_; exists $self->provides->{$module}; } sub version_for { my($self, $module) = @_; $self->provides->{$module}{version}; } 1; CARTON_DIST $fatpacked{"Carton/Dist/Core.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_DIST_CORE'; package Carton::Dist::Core; use strict; use parent 'Carton::Dist'; use Class::Tiny qw( module_version ); sub BUILDARGS { my($class, %args) = @_; # TODO represent dual-life $args{name} =~ s/::/-/g; \%args; } sub is_core { 1 } sub version_for { my($self, $module) = @_; $self->module_version; } 1; CARTON_DIST_CORE $fatpacked{"Carton/Environment.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_ENVIRONMENT'; package Carton::Environment; use strict; use Carton::CPANfile; use Carton::Snapshot; use Carton::Error; use Carton::Tree; use Path::Tiny; use Class::Tiny { cpanfile => undef, snapshot => sub { $_[0]->_build_snapshot }, install_path => sub { $_[0]->_build_install_path }, vendor_cache => sub { $_[0]->_build_vendor_cache }, tree => sub { $_[0]->_build_tree }, }; sub _build_snapshot { my $self = shift; Carton::Snapshot->new(path => $self->cpanfile . ".snapshot"); } sub _build_install_path { my $self = shift; if ($ENV{PERL_CARTON_PATH}) { return Path::Tiny->new($ENV{PERL_CARTON_PATH}); } else { return $self->cpanfile->path->parent->child("local"); } } sub _build_vendor_cache { my $self = shift; Path::Tiny->new($self->install_path->dirname . "/vendor/cache"); } sub _build_tree { my $self = shift; Carton::Tree->new(cpanfile => $self->cpanfile, snapshot => $self->snapshot); } sub vendor_bin { my $self = shift; $self->vendor_cache->parent->child('bin'); } sub build_with { my($class, $cpanfile) = @_; $cpanfile = Path::Tiny->new($cpanfile)->absolute; if ($cpanfile->is_file) { return $class->new(cpanfile => Carton::CPANfile->new(path => $cpanfile)); } else { Carton::Error::CPANfileNotFound->throw(error => "Can't locate cpanfile: $cpanfile"); } } sub build { my($class, $cpanfile_path, $install_path) = @_; my $self = $class->new; $cpanfile_path &&= Path::Tiny->new($cpanfile_path)->absolute; my $cpanfile = $self->locate_cpanfile($cpanfile_path || $ENV{PERL_CARTON_CPANFILE}); if ($cpanfile && $cpanfile->is_file) { $self->cpanfile( Carton::CPANfile->new(path => $cpanfile) ); } else { Carton::Error::CPANfileNotFound->throw(error => "Can't locate cpanfile: (@{[ $cpanfile_path || 'cpanfile' ]})"); } $self->install_path( Path::Tiny->new($install_path)->absolute ) if $install_path; $self; } sub locate_cpanfile { my($self, $path) = @_; if ($path) { return Path::Tiny->new($path)->absolute; } my $current = Path::Tiny->cwd; my $previous = ''; until ($current eq '/' or $current eq $previous) { # TODO support PERL_CARTON_CPANFILE my $try = $current->child('cpanfile'); if ($try->is_file) { return $try->absolute; } ($previous, $current) = ($current, $current->parent); } return; } 1; CARTON_ENVIRONMENT $fatpacked{"Carton/Error.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_ERROR'; package Carton::Error; use strict; use overload '""' => sub { $_[0]->error }; use Carp; sub throw { my($class, @args) = @_; die $class->new(@args); } sub rethrow { die $_[0]; } sub new { my($class, %args) = @_; bless \%args, $class; } sub error { $_[0]->{error} || ref $_[0]; } package Carton::Error::CommandNotFound; use parent 'Carton::Error'; package Carton::Error::CommandExit; use parent 'Carton::Error'; sub code { $_[0]->{code} } package Carton::Error::CPANfileNotFound; use parent 'Carton::Error'; package Carton::Error::SnapshotParseError; use parent 'Carton::Error'; sub path { $_[0]->{path} } package Carton::Error::SnapshotNotFound; use parent 'Carton::Error'; sub path { $_[0]->{path} } 1; CARTON_ERROR $fatpacked{"Carton/Index.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_INDEX'; package Carton::Index; use strict; use Class::Tiny { _packages => sub { +{} }, generator => sub { require Carton; "Carton $Carton::VERSION" }, }; sub add_package { my($self, $package) = @_; $self->_packages->{$package->name} = $package; # XXX ||= } sub count { my $self = shift; scalar keys %{$self->_packages}; } sub packages { my $self = shift; sort { lc $a->name cmp lc $b->name } values %{$self->_packages}; } sub write { my($self, $fh) = @_; print $fh <generator ]} Line-Count: @{[ $self->count ]} Last-Updated: @{[ scalar localtime ]} EOF for my $p ($self->packages) { print $fh $self->_format_line($p->name, $p->version_format, $p->pathname); } } sub _format_line { my($self, @row) = @_; # from PAUSE::mldistwatch::rewrite02 my $one = 30; my $two = 8; if (length $row[0] > $one) { $one += 8 - length $row[1]; $two = length $row[1]; } sprintf "%-${one}s %${two}s %s\n", @row; } sub pad { my($str, $len, $left) = @_; my $howmany = $len - length($str); return $str if $howmany <= 0; my $pad = " " x $howmany; return $left ? "$pad$str" : "$str$pad"; } 1; CARTON_INDEX $fatpacked{"Carton/Mirror.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_MIRROR'; package Carton::Mirror; use strict; use Class::Tiny qw( url ); our $DefaultMirror = 'http://cpan.metacpan.org/'; sub BUILDARGS { my($class, $url) = @_; return { url => $url }; } sub default { my $class = shift; $class->new($DefaultMirror); } sub is_default { my $self = shift; $self->url eq $DefaultMirror; } 1; CARTON_MIRROR $fatpacked{"Carton/Package.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_PACKAGE'; package Carton::Package; use strict; use Class::Tiny qw( name version pathname ); sub BUILDARGS { my($class, @args) = @_; return { name => $args[0], version => $args[1], pathname => $args[2] }; } sub version_format { my $self = shift; defined $self->version ? $self->version : 'undef'; } 1; CARTON_PACKAGE $fatpacked{"Carton/Packer.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_PACKER'; package Carton::Packer; use Class::Tiny; use warnings NONFATAL => 'all'; use App::FatPacker; use File::pushd (); use Path::Tiny (); use CPAN::Meta (); use File::Find (); sub fatpack_carton { my($self, $dir) = @_; my $temp = Path::Tiny->tempdir; my $pushd = File::pushd::pushd $temp; my $file = $temp->child('carton.pre.pl'); $file->spew(<<'EOF'); #!/usr/bin/env perl use strict; use 5.008001; use Carton::CLI; $Carton::Fatpacked = 1; exit Carton::CLI->new->run(@ARGV); EOF my $fatpacked = $self->do_fatpack($file); my $executable = $dir->child('carton'); warn "Bundling $executable\n"; $dir->mkpath; $executable->spew($fatpacked); chmod 0755, $executable; } sub do_fatpack { my($self, $file) = @_; my $packer = App::FatPacker->new; my @modules = split /\r?\n/, $packer->trace(args => [$file], use => $self->required_modules); my @packlists = $packer->packlists_containing(\@modules); $packer->packlists_to_tree(Path::Tiny->new('fatlib')->absolute, \@packlists); my $fatpacked = do { local $SIG{__WARN__} = sub {}; $packer->fatpack_file($file); }; # HACK: File::Spec bundled into arch in < 5.16, but is loadable as pure-perl use Config; $fatpacked =~ s/\$fatpacked\{"$Config{archname}\/(Cwd|File)/\$fatpacked{"$1/g; $fatpacked; } sub required_modules { my $self = shift; my %requirements; for my $dist (qw( Carton Menlo-Legacy Menlo )) { $requirements{$_} = 1 for $self->required_modules_for($dist); } # these modules are needed, but lazy-loaded, so FatPacker wont bundle them by default. my @extra = qw(Menlo::Index::Mirror); [ keys %requirements, @extra ]; } sub required_modules_for { my($self, $dist) = @_; my $meta = $self->installed_meta($dist) or die "Couldn't find install metadata for $dist"; my %excludes = ( perl => 1, 'ExtUtils::MakeMaker' => 1, 'Module::Build' => 1, ); grep !$excludes{$_}, $meta->effective_prereqs->requirements_for('runtime', 'requires')->required_modules; } sub installed_meta { my($self, $dist) = @_; my @meta; my $finder = sub { if (m!\b$dist-.*[\\/]MYMETA.json!) { my $meta = CPAN::Meta->load_file($_); push @meta, $meta if $meta->name eq $dist; } }; my @meta_dirs = grep -d, map "$_/.meta", @INC; File::Find::find({ wanted => $finder, no_chdir => 1 }, @meta_dirs) if @meta_dirs; # return the latest version @meta = sort { version->new($b->version) cmp version->new($a->version) } @meta; return $meta[0]; } 1; CARTON_PACKER $fatpacked{"Carton/Snapshot.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_SNAPSHOT'; package Carton::Snapshot; use strict; use Config; use Carton::Dist; use Carton::Dist::Core; use Carton::Error; use Carton::Package; use Carton::Index; use Carton::Util; use Carton::Snapshot::Emitter; use Carton::Snapshot::Parser; use CPAN::Meta; use CPAN::Meta::Requirements; use File::Find (); use Try::Tiny; use Path::Tiny (); use Module::CoreList; use constant CARTON_SNAPSHOT_VERSION => '1.0'; use subs 'path'; use Class::Tiny { path => undef, version => sub { CARTON_SNAPSHOT_VERSION }, loaded => undef, _distributions => sub { +[] }, }; sub BUILD { my $self = shift; $self->path( $self->{path} ); } sub path { my $self = shift; if (@_) { $self->{path} = Path::Tiny->new($_[0]); } else { $self->{path}; } } sub load_if_exists { my $self = shift; $self->load if $self->path->is_file; } sub load { my $self = shift; return 1 if $self->loaded; if ($self->path->is_file) { my $parser = Carton::Snapshot::Parser->new; $parser->parse($self->path->slurp_utf8, $self); $self->loaded(1); return 1; } else { Carton::Error::SnapshotNotFound->throw( error => "Can't find cpanfile.snapshot: Run `carton install` to build the snapshot file.", path => $self->path, ); } } sub save { my $self = shift; $self->path->spew_utf8( Carton::Snapshot::Emitter->new->emit($self) ); } sub find { my($self, $module) = @_; (grep $_->provides_module($module), $self->distributions)[0]; } sub find_or_core { my($self, $module) = @_; $self->find($module) || $self->find_in_core($module); } sub find_in_core { my($self, $module) = @_; if (exists $Module::CoreList::version{$]}{$module}) { my $version = $Module::CoreList::version{$]}{$module}; # maybe undef return Carton::Dist::Core->new(name => $module, module_version => $version); } return; } sub index { my $self = shift; my $index = Carton::Index->new; for my $package ($self->packages) { $index->add_package($package); } return $index; } sub distributions { @{$_[0]->_distributions}; } sub add_distribution { my($self, $dist) = @_; push @{$self->_distributions}, $dist; } sub packages { my $self = shift; my @packages; for my $dist ($self->distributions) { while (my($package, $provides) = each %{$dist->provides}) { # TODO what if duplicates? push @packages, Carton::Package->new($package, $provides->{version}, $dist->pathname); } } return @packages; } sub write_index { my($self, $file) = @_; open my $fh, ">", $file or die $!; $self->index->write($fh); } sub find_installs { my($self, $path, $reqs) = @_; my $libdir = "$path/lib/perl5/$Config{archname}/.meta"; return {} unless -e $libdir; my @installs; my $wanted = sub { if ($_ eq 'install.json') { push @installs, [ $File::Find::name, "$File::Find::dir/MYMETA.json" ]; } }; File::Find::find($wanted, $libdir); my %installs; my $accepts = sub { my $module = shift; return 0 unless $reqs->accepts_module($module->{name}, $module->{provides}{$module->{name}}{version}); if (my $exist = $installs{$module->{name}}) { my $old_ver = version::->new($exist->{provides}{$module->{name}}{version}); my $new_ver = version::->new($module->{provides}{$module->{name}}{version}); return $new_ver >= $old_ver; } else { return 1; } }; for my $file (@installs) { my $module = Carton::Util::load_json($file->[0]); my $prereqs = -f $file->[1] ? CPAN::Meta->load_file($file->[1])->effective_prereqs : CPAN::Meta::Prereqs->new; my $reqs = CPAN::Meta::Requirements->new; $reqs->add_requirements($prereqs->requirements_for($_, 'requires')) for qw( configure build runtime ); if ($accepts->($module)) { $installs{$module->{name}} = Carton::Dist->new( name => $module->{dist}, pathname => $module->{pathname}, provides => $module->{provides}, version => $module->{version}, requirements => $reqs, ); } } my @new_dists; for my $module (sort keys %installs) { push @new_dists, $installs{$module}; } $self->_distributions(\@new_dists); } 1; CARTON_SNAPSHOT $fatpacked{"Carton/Snapshot/Emitter.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_SNAPSHOT_EMITTER'; package Carton::Snapshot::Emitter; use Class::Tiny; use warnings NONFATAL => 'all'; sub emit { my($self, $snapshot) = @_; my $data = ''; $data .= "# carton snapshot format: version @{[$snapshot->version]}\n"; $data .= "DISTRIBUTIONS\n"; for my $dist (sort { $a->name cmp $b->name } $snapshot->distributions) { $data .= " @{[$dist->name]}\n"; $data .= " pathname: @{[$dist->pathname]}\n"; $data .= " provides:\n"; for my $package (sort keys %{$dist->provides}) { my $version = $dist->provides->{$package}{version}; $version = 'undef' unless defined $version; $data .= " $package $version\n"; } $data .= " requirements:\n"; for my $module (sort $dist->required_modules) { $data .= " $module @{[ $dist->requirements_for_module($module) || '0' ]}\n"; } } $data; } 1; CARTON_SNAPSHOT_EMITTER $fatpacked{"Carton/Snapshot/Parser.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_SNAPSHOT_PARSER'; package Carton::Snapshot::Parser; use Class::Tiny; use warnings NONFATAL => 'all'; use Carton::Dist; use Carton::Error; my $machine = { init => [ { re => qr/^\# carton snapshot format: version (1\.0)/, code => sub { my($stash, $snapshot, $ver) = @_; $snapshot->version($ver); }, goto => 'section', }, # TODO support pasing error and version mismatch etc. ], section => [ { re => qr/^DISTRIBUTIONS$/, goto => 'dists', }, { re => qr/^__EOF__$/, done => 1, }, ], dists => [ { re => qr/^ (\S+)$/, code => sub { $_[0]->{dist} = Carton::Dist->new(name => $1) }, goto => 'distmeta', }, { re => qr/^\S/, goto => 'section', redo => 1, }, ], distmeta => [ { re => qr/^ pathname: (.*)$/, code => sub { $_[0]->{dist}->pathname($1) }, }, { re => qr/^\s{4}provides:$/, code => sub { $_[0]->{property} = 'provides' }, goto => 'properties', }, { re => qr/^\s{4}requirements:$/, code => sub { $_[0]->{property} = 'requirements'; }, goto => 'properties', }, { re => qr/^\s{0,2}\S/, code => sub { my($stash, $snapshot) = @_; $snapshot->add_distribution($stash->{dist}); %$stash = (); # clear }, goto => 'dists', redo => 1, }, ], properties => [ { re => qr/^\s{6}([0-9A-Za-z_:]+) ([v0-9\._,=\!<>\s]+|undef)/, code => sub { my($stash, $snapshot, $module, $version) = @_; if ($stash->{property} eq 'provides') { $stash->{dist}->provides->{$module} = { version => $version }; } else { $stash->{dist}->add_string_requirement($module, $version); } }, }, { re => qr/^\s{0,4}\S/, goto => 'distmeta', redo => 1, }, ], }; sub parse { my($self, $data, $snapshot) = @_; my @lines = split /\r?\n/, $data; my $state = $machine->{init}; my $stash = {}; LINE: for my $line (@lines, '__EOF__') { last LINE unless @$state; STATE: { for my $trans (@{$state}) { if (my @match = $line =~ $trans->{re}) { if (my $code = $trans->{code}) { $code->($stash, $snapshot, @match); } if (my $goto = $trans->{goto}) { $state = $machine->{$goto}; if ($trans->{redo}) { redo STATE; } else { next LINE; } } last STATE; } } Carton::Error::SnapshotParseError->throw(error => "Could not parse snapshot file: $line"); } } } 1; CARTON_SNAPSHOT_PARSER $fatpacked{"Carton/Tree.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_TREE'; package Carton::Tree; use strict; use Carton::Dependency; use Class::Tiny qw( cpanfile snapshot ); use constant STOP => -1; sub walk_down { my($self, $cb) = @_; my $dumper; $dumper = sub { my($dependency, $reqs, $level, $parent) = @_; my $ret = $cb->($dependency, $reqs, $level); return if $ret && $ret == STOP; local $parent->{$dependency->distname} = 1 if $dependency; for my $module (sort $reqs->required_modules) { my $dependency = $self->dependency_for($module, $reqs); if ($dependency->dist) { next if $parent->{$dependency->distname}; $dumper->($dependency, $dependency->requirements, $level + 1, $parent); } else { # no dist found in lock } } }; $dumper->(undef, $self->cpanfile->requirements, 0, {}); undef $dumper; } sub dependency_for { my($self, $module, $reqs) = @_; my $requirement = $reqs->requirements_for_module($module); my $dep = Carton::Dependency->new; $dep->module($module); $dep->requirement($requirement); if (my $dist = $self->snapshot->find_or_core($module)) { $dep->dist($dist); } return $dep; } sub merged_requirements { my $self = shift; my $merged_reqs = CPAN::Meta::Requirements->new; my %seen; $self->walk_down(sub { my($dependency, $reqs, $level) = @_; return Carton::Tree::STOP if $dependency && $seen{$dependency->distname}++; $merged_reqs->add_requirements($reqs); }); $merged_reqs->clear_requirement('perl'); $merged_reqs->finalize; $merged_reqs; } 1; CARTON_TREE $fatpacked{"Carton/Util.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CARTON_UTIL'; package Carton::Util; use strict; use warnings; sub load_json { my $file = shift; open my $fh, "<", $file or die "$file: $!"; from_json(join '', <$fh>); } sub dump_json { my($data, $file) = @_; open my $fh, ">", $file or die "$file: $!"; binmode $fh; print $fh to_json($data); } sub from_json { require JSON::PP; JSON::PP->new->utf8->decode($_[0]) } sub to_json { my($data) = @_; require JSON::PP; JSON::PP->new->utf8->pretty->canonical->encode($data); } 1; CARTON_UTIL $fatpacked{"Class/Tiny.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'CLASS_TINY'; use 5.006; use strict; no strict 'refs'; use warnings; package Class::Tiny; # ABSTRACT: Minimalist class construction our $VERSION = '1.006'; use Carp (); # load as .pm to hide from min version scanners require( $] >= 5.010 ? "mro.pm" : "MRO/Compat.pm" ); ## no critic: my %CLASS_ATTRIBUTES; sub import { my $class = shift; my $pkg = caller; $class->prepare_class($pkg); $class->create_attributes( $pkg, @_ ) if @_; } sub prepare_class { my ( $class, $pkg ) = @_; @{"${pkg}::ISA"} = "Class::Tiny::Object" unless @{"${pkg}::ISA"}; } # adapted from Object::Tiny and Object::Tiny::RW sub create_attributes { my ( $class, $pkg, @spec ) = @_; my %defaults = map { ref $_ eq 'HASH' ? %$_ : ( $_ => undef ) } @spec; my @attr = grep { defined and !ref and /^[^\W\d]\w*$/s or Carp::croak "Invalid accessor name '$_'" } keys %defaults; $CLASS_ATTRIBUTES{$pkg}{$_} = $defaults{$_} for @attr; $class->_gen_accessor( $pkg, $_ ) for grep { !*{"$pkg\::$_"}{CODE} } @attr; Carp::croak("Failed to generate attributes for $pkg: $@\n") if $@; } sub _gen_accessor { my ( $class, $pkg, $name ) = @_; my $outer_default = $CLASS_ATTRIBUTES{$pkg}{$name}; my $sub = $class->__gen_sub_body( $name, defined($outer_default), ref($outer_default) ); # default = outer_default avoids "won't stay shared" bug eval "package $pkg; my \$default=\$outer_default; $sub"; ## no critic Carp::croak("Failed to generate attributes for $pkg: $@\n") if $@; } # NOTE: overriding __gen_sub_body in a subclass of Class::Tiny is risky and # could break if the internals of Class::Tiny need to change for any # reason. That said, I currently see no reason why this would be likely to # change. # # The generated sub body should assume that a '$default' variable will be # in scope (i.e. when the sub is evaluated) with any default value/coderef sub __gen_sub_body { my ( $self, $name, $has_default, $default_type ) = @_; if ( $has_default && $default_type eq 'CODE' ) { return << "HERE"; sub $name { return ( ( \@_ == 1 && exists \$_[0]{$name} ) ? ( \$_[0]{$name} ) : ( \$_[0]{$name} = ( \@_ == 2 ) ? \$_[1] : \$default->( \$_[0] ) ) ); } HERE } elsif ($has_default) { return << "HERE"; sub $name { return ( ( \@_ == 1 && exists \$_[0]{$name} ) ? ( \$_[0]{$name} ) : ( \$_[0]{$name} = ( \@_ == 2 ) ? \$_[1] : \$default ) ); } HERE } else { return << "HERE"; sub $name { return \@_ == 1 ? \$_[0]{$name} : ( \$_[0]{$name} = \$_[1] ); } HERE } } sub get_all_attributes_for { my ( $class, $pkg ) = @_; my %attr = map { $_ => undef } map { keys %{ $CLASS_ATTRIBUTES{$_} || {} } } @{ mro::get_linear_isa($pkg) }; return keys %attr; } sub get_all_attribute_defaults_for { my ( $class, $pkg ) = @_; my $defaults = {}; for my $p ( reverse @{ mro::get_linear_isa($pkg) } ) { while ( my ( $k, $v ) = each %{ $CLASS_ATTRIBUTES{$p} || {} } ) { $defaults->{$k} = $v; } } return $defaults; } package Class::Tiny::Object; # ABSTRACT: Base class for classes built with Class::Tiny our $VERSION = '1.006'; my ( %HAS_BUILDARGS, %BUILD_CACHE, %DEMOLISH_CACHE, %ATTR_CACHE ); my $_PRECACHE = sub { no warnings 'once'; # needed to avoid downstream warnings my ($class) = @_; my $linear_isa = @{"$class\::ISA"} == 1 && ${"$class\::ISA"}[0] eq "Class::Tiny::Object" ? [$class] : mro::get_linear_isa($class); $DEMOLISH_CACHE{$class} = [ map { ( *{$_}{CODE} ) ? ( *{$_}{CODE} ) : () } map { "$_\::DEMOLISH" } @$linear_isa ]; $BUILD_CACHE{$class} = [ map { ( *{$_}{CODE} ) ? ( *{$_}{CODE} ) : () } map { "$_\::BUILD" } reverse @$linear_isa ]; $HAS_BUILDARGS{$class} = $class->can("BUILDARGS"); return $ATTR_CACHE{$class} = { map { $_ => 1 } Class::Tiny->get_all_attributes_for($class) }; }; sub new { my $class = shift; my $valid_attrs = $ATTR_CACHE{$class} || $_PRECACHE->($class); # handle hash ref or key/value arguments my $args; if ( $HAS_BUILDARGS{$class} ) { $args = $class->BUILDARGS(@_); } else { if ( @_ == 1 && ref $_[0] ) { my %copy = eval { %{ $_[0] } }; # try shallow copy Carp::croak("Argument to $class->new() could not be dereferenced as a hash") if $@; $args = \%copy; } elsif ( @_ % 2 == 0 ) { $args = {@_}; } else { Carp::croak("$class->new() got an odd number of elements"); } } # create object and invoke BUILD (unless we were given __no_BUILD__) my $self = bless { map { $_ => $args->{$_} } grep { exists $valid_attrs->{$_} } keys %$args }, $class; $self->BUILDALL($args) if !delete $args->{__no_BUILD__} && @{ $BUILD_CACHE{$class} }; return $self; } sub BUILDALL { $_->(@_) for @{ $BUILD_CACHE{ ref $_[0] } } } # Adapted from Moo and its dependencies require Devel::GlobalDestruction unless defined ${^GLOBAL_PHASE}; sub DESTROY { my $self = shift; my $class = ref $self; my $in_global_destruction = defined ${^GLOBAL_PHASE} ? ${^GLOBAL_PHASE} eq 'DESTRUCT' : Devel::GlobalDestruction::in_global_destruction(); for my $demolisher ( @{ $DEMOLISH_CACHE{$class} } ) { my $e = do { local ( $?, $@ ); eval { $demolisher->( $self, $in_global_destruction ) }; $@; }; no warnings 'misc'; # avoid (in cleanup) warnings die $e if $e; # rethrow } } 1; # vim: ts=4 sts=4 sw=4 et: __END__ =pod =encoding UTF-8 =head1 NAME Class::Tiny - Minimalist class construction =head1 VERSION version 1.006 =head1 SYNOPSIS In F: package Person; use Class::Tiny qw( name ); 1; In F: package Employee; use parent 'Person'; use Class::Tiny qw( ssn ), { timestamp => sub { time } # attribute with default }; 1; In F: use Employee; my $obj = Employee->new( name => "Larry", ssn => "111-22-3333" ); # unknown attributes are ignored my $obj = Employee->new( name => "Larry", OS => "Linux" ); # $obj->{OS} does not exist =head1 DESCRIPTION This module offers a minimalist class construction kit in around 120 lines of code. Here is a list of features: =over 4 =item * defines attributes via import arguments =item * generates read-write accessors =item * supports lazy attribute defaults =item * supports custom accessors =item * superclass provides a standard C constructor =item * C takes a hash reference or list of key/value pairs =item * C supports providing C to customize constructor options =item * C calls C for each class from parent to child =item * superclass provides a C method =item * C calls C for each class from child to parent =back Multiple-inheritance is possible, with superclass order determined via L. It uses no non-core modules for any recent Perl. On Perls older than v5.10 it requires L. On Perls older than v5.14, it requires L. =head1 USAGE =head2 Defining attributes Define attributes as a list of import arguments: package Foo::Bar; use Class::Tiny qw( name id height weight ); For each attribute, a read-write accessor is created unless a subroutine of that name already exists: $obj->name; # getter $obj->name( "John Doe" ); # setter Attribute names must be valid subroutine identifiers or an exception will be thrown. You can specify lazy defaults by defining attributes with a hash reference. Keys define attribute names and values are constants or code references that will be evaluated when the attribute is first accessed if no value has been set. The object is passed as an argument to a code reference. package Foo::WithDefaults; use Class::Tiny qw/name id/, { title => 'Peon', skills => sub { [] }, hire_date => sub { $_[0]->_build_hire_date }, }; When subclassing, if multiple accessors of the same name exist in different classes, any default (or lack of default) is determined by standard method resolution order. To make your own custom accessors, just pre-declare the method name before loading Class::Tiny: package Foo::Bar; use subs 'id'; use Class::Tiny qw( name id ); sub id { ... } Even if you pre-declare a method name, you must include it in the attribute list for Class::Tiny to register it as a valid attribute. If you set a default for a custom accessor, your accessor will need to retrieve the default and do something with it: package Foo::Bar; use subs 'id'; use Class::Tiny qw( name ), { id => sub { int(rand(2*31)) } }; sub id { my $self = shift; if (@_) { return $self->{id} = shift; } elsif ( exists $self->{id} ) { return $self->{id}; } else { my $defaults = Class::Tiny->get_all_attribute_defaults_for( ref $self ); return $self->{id} = $defaults->{id}->(); } } =head2 Class::Tiny::Object is your base class If your class B already inherit from some class, then Class::Tiny::Object will be added to your C<@ISA> to provide C and C. If your class B inherit from something, then no additional inheritance is set up. If the parent subclasses Class::Tiny::Object, then all is well. If not, then you'll get accessors set up but no constructor or destructor. Don't do that unless you really have a special need for it. Define subclasses as normal. It's best to define them with L, L or L before defining attributes with Class::Tiny so the C<@ISA> array is already populated at compile-time: package Foo::Bar::More; use parent 'Foo::Bar'; use Class::Tiny qw( shoe_size ); =head2 Object construction If your class inherits from Class::Tiny::Object (as it should if you followed the advice above), it provides the C constructor for you. Objects can be created with attributes given as a hash reference or as a list of key/value pairs: $obj = Foo::Bar->new( name => "David" ); $obj = Foo::Bar->new( { name => "David" } ); If a reference is passed as a single argument, it must be able to be dereferenced as a hash or an exception is thrown. Unknown attributes in the constructor arguments will be ignored. Prior to version 1.000, unknown attributes were an error, but this made it harder for people to cleanly subclass Class::Tiny classes so this feature was removed. You can define a C method to change how arguments to new are handled. It will receive the constructor arguments as they were provided and must return a hash reference of key/value pairs (or else throw an exception). sub BUILDARGS { my $class = shift; my $name = shift || "John Doe"; return { name => $name }; }; Foo::Bar->new( "David" ); Foo::Bar->new(); # "John Doe" Unknown attributes returned from C will be ignored. =head2 BUILD If your class or any superclass defines a C method, it will be called by the constructor from the furthest parent class down to the child class after the object has been created. It is passed the constructor arguments as a hash reference. The return value is ignored. Use C for validation, checking required attributes or setting default values that depend on other attributes. sub BUILD { my ($self, $args) = @_; for my $req ( qw/name age/ ) { croak "$req attribute required" unless defined $self->$req; } croak "Age must be non-negative" if $self->age < 0; $self->msg( "Hello " . $self->name ); } The argument reference is a copy, so deleting elements won't affect data in the original (but changes will be passed to other BUILD methods in C<@ISA>). =head2 DEMOLISH Class::Tiny provides a C method. If your class or any superclass defines a C method, they will be called from the child class to the furthest parent class during object destruction. It is provided a single boolean argument indicating whether Perl is in global destruction. Return values and errors are ignored. sub DEMOLISH { my ($self, $global_destruct) = @_; $self->cleanup(); } =head2 Introspection and internals You can retrieve an unsorted list of valid attributes known to Class::Tiny for a class and its superclasses with the C class method. my @attrs = Class::Tiny->get_all_attributes_for("Employee"); # returns qw/name ssn timestamp/ Likewise, a hash reference of all valid attributes and default values (or code references) may be retrieved with the C class method. Any attributes without a default will be C. my $def = Class::Tiny->get_all_attribute_defaults_for("Employee"); # returns { # name => undef, # ssn => undef # timestamp => $coderef # } The C method uses two class methods, C and C to set up the C<@ISA> array and attributes. Anyone attempting to extend Class::Tiny itself should use these instead of mocking up a call to C. When the first object is created, linearized C<@ISA>, the valid attribute list and various subroutine references are cached for speed. Ensure that all inheritance and methods are in place before creating objects. (You don't want to be changing that once you create objects anyway, right?) =for Pod::Coverage new get_all_attributes_for get_all_attribute_defaults_for prepare_class create_attributes =head1 RATIONALE =head2 Why this instead of Object::Tiny or Class::Accessor or something else? I wanted something so simple that it could potentially be used by core Perl modules I help maintain (or hope to write), most of which either use L or roll-their-own OO framework each time. L and L were close to what I wanted, but lacking some features I deemed necessary, and their maintainers have an even more strict philosophy against feature creep than I have. I also considered L, which has been around a long time and is heavily used, but it, too, lacked features I wanted and did things in ways I considered poor design. I looked for something else on CPAN, but after checking a dozen class creators I realized I could implement exactly what I wanted faster than I could search CPAN for something merely sufficient. In general, compared to most things on CPAN (other than Object::Tiny), Class::Tiny is smaller in implementation and simpler in API. Specifically, here is how Class::Tiny ("C::T") compares to Object::Tiny ("O::T") and Class::Accessor ("C::A"): FEATURE C::T O::T C::A -------------------------------------------------------------- attributes defined via import yes yes no read/write accessors yes no yes lazy attribute defaults yes no no provides new yes yes yes provides DESTROY yes no no new takes either hashref or list yes no (list) no (hash) Moo(se)-like BUILD/DEMOLISH yes no no Moo(se)-like BUILDARGS yes no no no extraneous methods via @ISA yes yes no =head2 Why this instead of Moose or Moo? L and L are both excellent OO frameworks. Moose offers a powerful meta-object protocol (MOP), but is slow to start up and has about 30 non-core dependencies including XS modules. Moo is faster to start up and has about 10 pure Perl dependencies but provides no true MOP, relying instead on its ability to transparently upgrade Moo to Moose when Moose's full feature set is required. By contrast, Class::Tiny has no MOP and has B non-core dependencies for Perls in the L. It has far less code, less complexity and no learning curve. If you don't need or can't afford what Moo or Moose offer, this is intended to be a reasonable fallback. That said, Class::Tiny offers Moose-like conventions for things like C and C for some minimal interoperability and an easier upgrade path. =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/dagolden/Class-Tiny.git =head1 AUTHOR David Golden =head1 CONTRIBUTORS =for stopwords Dagfinn Ilmari Mannsåker David Golden Gelu Lupas Karen Etheridge Olivier Mengué Toby Inkster =over 4 =item * Dagfinn Ilmari Mannsåker =item * David Golden =item * Gelu Lupas =item * Karen Etheridge =item * Olivier Mengué =item * Toby Inkster =back =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2013 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut CLASS_TINY $fatpacked{"ExtUtils/Command.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_COMMAND'; package ExtUtils::Command; use 5.00503; use strict; require Exporter; use vars qw(@ISA @EXPORT @EXPORT_OK $VERSION); @ISA = qw(Exporter); @EXPORT = qw(cp rm_f rm_rf mv cat eqtime mkpath touch test_f test_d chmod dos2unix); $VERSION = '7.36'; $VERSION =~ tr/_//d; my $Is_VMS = $^O eq 'VMS'; my $Is_VMS_mode = $Is_VMS; my $Is_VMS_noefs = $Is_VMS; my $Is_Win32 = $^O eq 'MSWin32'; if( $Is_VMS ) { my $vms_unix_rpt; my $vms_efs; my $vms_case; if (eval { local $SIG{__DIE__}; local @INC = @INC; pop @INC if $INC[-1] eq '.'; require VMS::Feature; }) { $vms_unix_rpt = VMS::Feature::current("filename_unix_report"); $vms_efs = VMS::Feature::current("efs_charset"); $vms_case = VMS::Feature::current("efs_case_preserve"); } else { my $unix_rpt = $ENV{'DECC$FILENAME_UNIX_REPORT'} || ''; my $efs_charset = $ENV{'DECC$EFS_CHARSET'} || ''; my $efs_case = $ENV{'DECC$EFS_CASE_PRESERVE'} || ''; $vms_unix_rpt = $unix_rpt =~ /^[ET1]/i; $vms_efs = $efs_charset =~ /^[ET1]/i; $vms_case = $efs_case =~ /^[ET1]/i; } $Is_VMS_mode = 0 if $vms_unix_rpt; $Is_VMS_noefs = 0 if ($vms_efs); } =head1 NAME ExtUtils::Command - utilities to replace common UNIX commands in Makefiles etc. =head1 SYNOPSIS perl -MExtUtils::Command -e cat files... > destination perl -MExtUtils::Command -e mv source... destination perl -MExtUtils::Command -e cp source... destination perl -MExtUtils::Command -e touch files... perl -MExtUtils::Command -e rm_f files... perl -MExtUtils::Command -e rm_rf directories... perl -MExtUtils::Command -e mkpath directories... perl -MExtUtils::Command -e eqtime source destination perl -MExtUtils::Command -e test_f file perl -MExtUtils::Command -e test_d directory perl -MExtUtils::Command -e chmod mode files... ... =head1 DESCRIPTION The module is used to replace common UNIX commands. In all cases the functions work from @ARGV rather than taking arguments. This makes them easier to deal with in Makefiles. Call them like this: perl -MExtUtils::Command -e some_command some files to work on and I like this: perl -MExtUtils::Command -e 'some_command qw(some files to work on)' For that use L. Filenames with * and ? will be glob expanded. =head2 FUNCTIONS =over 4 =cut # VMS uses % instead of ? to mean "one character" my $wild_regex = $Is_VMS ? '*%' : '*?'; sub expand_wildcards { @ARGV = map(/[$wild_regex]/o ? glob($_) : $_,@ARGV); } =item cat cat file ... Concatenates all files mentioned on command line to STDOUT. =cut sub cat () { expand_wildcards(); print while (<>); } =item eqtime eqtime source destination Sets modified time of destination to that of source. =cut sub eqtime { my ($src,$dst) = @ARGV; local @ARGV = ($dst); touch(); # in case $dst doesn't exist utime((stat($src))[8,9],$dst); } =item rm_rf rm_rf files or directories ... Removes files and directories - recursively (even if readonly) =cut sub rm_rf { expand_wildcards(); require File::Path; File::Path::rmtree([grep -e $_,@ARGV],0,0); } =item rm_f rm_f file ... Removes files (even if readonly) =cut sub rm_f { expand_wildcards(); foreach my $file (@ARGV) { next unless -f $file; next if _unlink($file); chmod(0777, $file); next if _unlink($file); require Carp; Carp::carp("Cannot delete $file: $!"); } } sub _unlink { my $files_unlinked = 0; foreach my $file (@_) { my $delete_count = 0; $delete_count++ while unlink $file; $files_unlinked++ if $delete_count; } return $files_unlinked; } =item touch touch file ... Makes files exist, with current timestamp =cut sub touch { my $t = time; expand_wildcards(); foreach my $file (@ARGV) { open(FILE,">>$file") || die "Cannot write $file:$!"; close(FILE); utime($t,$t,$file); } } =item mv mv source_file destination_file mv source_file source_file destination_dir Moves source to destination. Multiple sources are allowed if destination is an existing directory. Returns true if all moves succeeded, false otherwise. =cut sub mv { expand_wildcards(); my @src = @ARGV; my $dst = pop @src; if (@src > 1 && ! -d $dst) { require Carp; Carp::croak("Too many arguments"); } require File::Copy; my $nok = 0; foreach my $src (@src) { $nok ||= !File::Copy::move($src,$dst); } return !$nok; } =item cp cp source_file destination_file cp source_file source_file destination_dir Copies sources to the destination. Multiple sources are allowed if destination is an existing directory. Returns true if all copies succeeded, false otherwise. =cut sub cp { expand_wildcards(); my @src = @ARGV; my $dst = pop @src; if (@src > 1 && ! -d $dst) { require Carp; Carp::croak("Too many arguments"); } require File::Copy; my $nok = 0; foreach my $src (@src) { $nok ||= !File::Copy::copy($src,$dst); # Win32 does not update the mod time of a copied file, just the # created time which make does not look at. utime(time, time, $dst) if $Is_Win32; } return $nok; } =item chmod chmod mode files ... Sets UNIX like permissions 'mode' on all the files. e.g. 0666 =cut sub chmod { local @ARGV = @ARGV; my $mode = shift(@ARGV); expand_wildcards(); if( $Is_VMS_mode && $Is_VMS_noefs) { require File::Spec; foreach my $idx (0..$#ARGV) { my $path = $ARGV[$idx]; next unless -d $path; # chmod 0777, [.foo.bar] doesn't work on VMS, you have to do # chmod 0777, [.foo]bar.dir my @dirs = File::Spec->splitdir( $path ); $dirs[-1] .= '.dir'; $path = File::Spec->catfile(@dirs); $ARGV[$idx] = $path; } } chmod(oct $mode,@ARGV) || die "Cannot chmod ".join(' ',$mode,@ARGV).":$!"; } =item mkpath mkpath directory ... Creates directories, including any parent directories. =cut sub mkpath { expand_wildcards(); require File::Path; File::Path::mkpath([@ARGV],0,0777); } =item test_f test_f file Tests if a file exists. I with 0 if it does, 1 if it does not (ie. shell's idea of true and false). =cut sub test_f { exit(-f $ARGV[0] ? 0 : 1); } =item test_d test_d directory Tests if a directory exists. I with 0 if it does, 1 if it does not (ie. shell's idea of true and false). =cut sub test_d { exit(-d $ARGV[0] ? 0 : 1); } =item dos2unix dos2unix files or dirs ... Converts DOS and OS/2 linefeeds to Unix style recursively. =cut sub dos2unix { require File::Find; File::Find::find(sub { return if -d; return unless -w _; return unless -r _; return if -B _; local $\; my $orig = $_; my $temp = '.dos2unix_tmp'; open ORIG, $_ or do { warn "dos2unix can't open $_: $!"; return }; open TEMP, ">$temp" or do { warn "dos2unix can't create .dos2unix_tmp: $!"; return }; binmode ORIG; binmode TEMP; while (my $line = ) { $line =~ s/\015\012/\012/g; print TEMP $line; } close ORIG; close TEMP; rename $temp, $orig; }, @ARGV); } =back =head1 SEE ALSO Shell::Command which is these same functions but take arguments normally. =head1 AUTHOR Nick Ing-Simmons C Maintained by Michael G Schwern C within the ExtUtils-MakeMaker package and, as a separate CPAN package, by Randy Kobes C. =cut EXTUTILS_COMMAND $fatpacked{"ExtUtils/Command/MM.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_COMMAND_MM'; package ExtUtils::Command::MM; require 5.006; use strict; use warnings; require Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(test_harness pod2man perllocal_install uninstall warn_if_old_packlist test_s cp_nonempty); our $VERSION = '7.36'; $VERSION =~ tr/_//d; my $Is_VMS = $^O eq 'VMS'; sub mtime { no warnings 'redefine'; local $@; *mtime = (eval { require Time::HiRes } && defined &Time::HiRes::stat) ? sub { (Time::HiRes::stat($_[0]))[9] } : sub { ( stat($_[0]))[9] } ; goto &mtime; } =head1 NAME ExtUtils::Command::MM - Commands for the MM's to use in Makefiles =head1 SYNOPSIS perl "-MExtUtils::Command::MM" -e "function" "--" arguments... =head1 DESCRIPTION B The interface is not stable. ExtUtils::Command::MM encapsulates code which would otherwise have to be done with large "one" liners. Any $(FOO) used in the examples are make variables, not Perl. =over 4 =item B test_harness($verbose, @test_libs); Runs the tests on @ARGV via Test::Harness passing through the $verbose flag. Any @test_libs will be unshifted onto the test's @INC. @test_libs are run in alphabetical order. =cut sub test_harness { require Test::Harness; require File::Spec; $Test::Harness::verbose = shift; # Because Windows doesn't do this for us and listing all the *.t files # out on the command line can blow over its exec limit. require ExtUtils::Command; my @argv = ExtUtils::Command::expand_wildcards(@ARGV); local @INC = @INC; unshift @INC, map { File::Spec->rel2abs($_) } @_; Test::Harness::runtests(sort { lc $a cmp lc $b } @argv); } =item B pod2man( '--option=value', $podfile1 => $manpage1, $podfile2 => $manpage2, ... ); # or args on @ARGV pod2man() is a function performing most of the duties of the pod2man program. Its arguments are exactly the same as pod2man as of 5.8.0 with the addition of: --perm_rw octal permission to set the resulting manpage to And the removal of: --verbose/-v --help/-h If no arguments are given to pod2man it will read from @ARGV. If Pod::Man is unavailable, this function will warn and return undef. =cut sub pod2man { local @ARGV = @_ ? @_ : @ARGV; { local $@; if( !eval { require Pod::Man } ) { warn "Pod::Man is not available: $@". "Man pages will not be generated during this install.\n"; return 0; } } require Getopt::Long; # We will cheat and just use Getopt::Long. We fool it by putting # our arguments into @ARGV. Should be safe. my %options = (); Getopt::Long::config ('bundling_override'); Getopt::Long::GetOptions (\%options, 'section|s=s', 'release|r=s', 'center|c=s', 'date|d=s', 'fixed=s', 'fixedbold=s', 'fixeditalic=s', 'fixedbolditalic=s', 'official|o', 'quotes|q=s', 'lax|l', 'name|n=s', 'perm_rw=i', 'utf8|u' ); delete $options{utf8} unless $Pod::Man::VERSION >= 2.17; # If there's no files, don't bother going further. return 0 unless @ARGV; # Official sets --center, but don't override things explicitly set. if ($options{official} && !defined $options{center}) { $options{center} = q[Perl Programmer's Reference Guide]; } # This isn't a valid Pod::Man option and is only accepted for backwards # compatibility. delete $options{lax}; my $count = scalar @ARGV / 2; my $plural = $count == 1 ? 'document' : 'documents'; print "Manifying $count pod $plural\n"; do {{ # so 'next' works my ($pod, $man) = splice(@ARGV, 0, 2); next if ((-e $man) && (mtime($man) > mtime($pod)) && (mtime($man) > mtime("Makefile"))); my $parser = Pod::Man->new(%options); $parser->parse_from_file($pod, $man) or do { warn("Could not install $man\n"); next }; if (exists $options{perm_rw}) { chmod(oct($options{perm_rw}), $man) or do { warn("chmod $options{perm_rw} $man: $!\n"); next }; } }} while @ARGV; return 1; } =item B perl "-MExtUtils::Command::MM" -e warn_if_old_packlist Displays a warning that an old packlist file was found. Reads the filename from @ARGV. =cut sub warn_if_old_packlist { my $packlist = $ARGV[0]; return unless -f $packlist; print <<"PACKLIST_WARNING"; WARNING: I have found an old package in $packlist. Please make sure the two installations are not conflicting PACKLIST_WARNING } =item B perl "-MExtUtils::Command::MM" -e perllocal_install ... # VMS only, key|value pairs come on STDIN perl "-MExtUtils::Command::MM" -e perllocal_install < | ... Prints a fragment of POD suitable for appending to perllocal.pod. Arguments are read from @ARGV. 'type' is the type of what you're installing. Usually 'Module'. 'module name' is simply the name of your module. (Foo::Bar) Key/value pairs are extra information about the module. Fields include: installed into which directory your module was out into LINKTYPE dynamic or static linking VERSION module version number EXE_FILES any executables installed in a space separated list =cut sub perllocal_install { my($type, $name) = splice(@ARGV, 0, 2); # VMS feeds args as a piped file on STDIN since it usually can't # fit all the args on a single command line. my @mod_info = $Is_VMS ? split /\|/, : @ARGV; my $pod; my $time = gmtime($ENV{SOURCE_DATE_EPOCH} || time); $pod = sprintf <<'POD', scalar($time), $type, $name, $name; =head2 %s: C<%s> L<%s|%s> =over 4 POD do { my($key, $val) = splice(@mod_info, 0, 2); $pod .= < POD } while(@mod_info); $pod .= "=back\n\n"; $pod =~ s/^ //mg; print $pod; return 1; } =item B perl "-MExtUtils::Command::MM" -e uninstall A wrapper around ExtUtils::Install::uninstall(). Warns that uninstallation is deprecated and doesn't actually perform the uninstallation. =cut sub uninstall { my($packlist) = shift @ARGV; require ExtUtils::Install; print <<'WARNING'; Uninstall is unsafe and deprecated, the uninstallation was not performed. We will show what would have been done. WARNING ExtUtils::Install::uninstall($packlist, 1, 1); print <<'WARNING'; Uninstall is unsafe and deprecated, the uninstallation was not performed. Please check the list above carefully, there may be errors. Remove the appropriate files manually. Sorry for the inconvenience. WARNING } =item B perl "-MExtUtils::Command::MM" -e test_s Tests if a file exists and is not empty (size > 0). I with 0 if it does, 1 if it does not. =cut sub test_s { exit(-s $ARGV[0] ? 0 : 1); } =item B perl "-MExtUtils::Command::MM" -e cp_nonempty Tests if the source file exists and is not empty (size > 0). If it is not empty it copies it to the given destination with the given permissions. =back =cut sub cp_nonempty { my @args = @ARGV; return 0 unless -s $args[0]; require ExtUtils::Command; { local @ARGV = @args[0,1]; ExtUtils::Command::cp(@ARGV); } { local @ARGV = @args[2,1]; ExtUtils::Command::chmod(@ARGV); } } 1; EXTUTILS_COMMAND_MM $fatpacked{"ExtUtils/Config.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_CONFIG'; package ExtUtils::Config; $ExtUtils::Config::VERSION = '0.008'; use strict; use warnings; use Config; use Data::Dumper (); sub new { my ($pack, $args) = @_; return bless { values => ($args ? { %$args } : {}), }, $pack; } sub get { my ($self, $key) = @_; return exists $self->{values}{$key} ? $self->{values}{$key} : $Config{$key}; } sub exists { my ($self, $key) = @_; return exists $self->{values}{$key} || exists $Config{$key}; } sub values_set { my $self = shift; return { %{$self->{values}} }; } sub all_config { my $self = shift; return { %Config, %{ $self->{values}} }; } sub serialize { my $self = shift; return $self->{serialized} ||= Data::Dumper->new([$self->values_set])->Terse(1)->Sortkeys(1)->Dump; } 1; # ABSTRACT: A wrapper for perl's configuration __END__ =pod =encoding UTF-8 =head1 NAME ExtUtils::Config - A wrapper for perl's configuration =head1 VERSION version 0.008 =head1 SYNOPSIS my $config = ExtUtils::Config->new(); $config->get('installsitelib'); =head1 DESCRIPTION ExtUtils::Config is an abstraction around the %Config hash. By itself it is not a particularly interesting module by any measure, however it ties together a family of modern toolchain modules. =head1 METHODS =head2 new(\%config) Create a new ExtUtils::Config object. The values in C<\%config> are used to initialize the object. =head2 get($key) Get the value of C<$key>. If not overridden it will return the value in %Config. =head2 exists($key) Tests for the existence of $key. =head2 values_set() Get a hashref of all overridden values. =head2 all_config() Get a hashref of the complete configuration, including overrides. =head2 serialize() This method serializes the object to some kind of string. =head1 AUTHORS =over 4 =item * Ken Williams =item * Leon Timmermans =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2006 by Ken Williams, Leon Timmermans. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut EXTUTILS_CONFIG $fatpacked{"ExtUtils/Helpers.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_HELPERS'; package ExtUtils::Helpers; $ExtUtils::Helpers::VERSION = '0.026'; use strict; use warnings FATAL => 'all'; use Exporter 5.57 'import'; use Config; use File::Basename qw/basename/; use File::Spec::Functions qw/splitpath canonpath abs2rel splitdir/; use Text::ParseWords 3.24 (); our @EXPORT_OK = qw/make_executable split_like_shell man1_pagename man3_pagename detildefy/; BEGIN { my %impl_for = ( MSWin32 => 'Windows', VMS => 'VMS'); my $package = 'ExtUtils::Helpers::' . ($impl_for{$^O} || 'Unix'); my $impl = $impl_for{$^O} || 'Unix'; require "ExtUtils/Helpers/$impl.pm"; "ExtUtils::Helpers::$impl"->import(); } sub split_like_shell { my ($string) = @_; return if not defined $string; $string =~ s/^\s+|\s+$//g; return if not length $string; return Text::ParseWords::shellwords($string); } sub man1_pagename { my $filename = shift; return basename($filename).".$Config{man1ext}"; } my %separator = ( MSWin32 => '.', VMS => '__', os2 => '.', cygwin => '.', ); my $separator = $separator{$^O} || '::'; sub man3_pagename { my ($filename, $base) = @_; $base ||= 'lib'; my ($vols, $dirs, $file) = splitpath(canonpath(abs2rel($filename, $base))); $file = basename($file, qw/.pm .pod/); my @dirs = grep { length } splitdir($dirs); return join $separator, @dirs, "$file.$Config{man3ext}"; } 1; # ABSTRACT: Various portability utilities for module builders __END__ =pod =encoding utf-8 =head1 NAME ExtUtils::Helpers - Various portability utilities for module builders =head1 VERSION version 0.026 =head1 SYNOPSIS use ExtUtils::Helpers qw/make_executable split_like_shell/; unshift @ARGV, split_like_shell($ENV{PROGRAM_OPTS}); write_script_to('Build'); make_executable('Build'); =head1 DESCRIPTION This module provides various portable helper functions for module building modules. =head1 FUNCTIONS =head2 make_executable($filename) This makes a perl script executable. =head2 split_like_shell($string) This function splits a string the same way as the local platform does. =head2 detildefy($path) This function substitutes a tilde at the start of a path with the users homedir in an appropriate manner. =head2 man1_pagename($filename) Returns the man page filename for a script. =head2 man3_pagename($filename, $basedir) Returns the man page filename for a Perl library. =head1 ACKNOWLEDGEMENTS Olivier Mengué and Christian Walde made C work on Windows. =head1 AUTHORS =over 4 =item * Ken Williams =item * Leon Timmermans =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2004 by Ken Williams, Leon Timmermans. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut EXTUTILS_HELPERS $fatpacked{"ExtUtils/Helpers/Unix.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_HELPERS_UNIX'; package ExtUtils::Helpers::Unix; $ExtUtils::Helpers::Unix::VERSION = '0.026'; use strict; use warnings FATAL => 'all'; use Exporter 5.57 'import'; our @EXPORT = qw/make_executable detildefy/; use Carp qw/croak/; use Config; my $layer = $] >= 5.008001 ? ":raw" : ""; sub make_executable { my $filename = shift; my $current_mode = (stat $filename)[2] + 0; if (-T $filename) { open my $fh, "<$layer", $filename; my @lines = <$fh>; if (@lines and $lines[0] =~ s{ \A \#! \s* (?:/\S+/)? perl \b (.*) \z }{$Config{startperl}$1}xms) { open my $out, ">$layer", "$filename.new" or croak "Couldn't open $filename.new: $!"; print $out @lines; close $out; rename $filename, "$filename.bak" or croak "Couldn't rename $filename to $filename.bak"; rename "$filename.new", $filename or croak "Couldn't rename $filename.new to $filename"; unlink "$filename.bak"; } } chmod $current_mode | oct(111), $filename; return; } sub detildefy { my $value = shift; # tilde with optional username for ($value) { s{ ^ ~ (?= /|$)} [ $ENV{HOME} || (getpwuid $>)[7] ]ex or # tilde without user name s{ ^ ~ ([^/]+) (?= /|$) } { (getpwnam $1)[7] || "~$1" }ex; # tilde with user name } return $value; } 1; # ABSTRACT: Unix specific helper bits __END__ =pod =encoding UTF-8 =head1 NAME ExtUtils::Helpers::Unix - Unix specific helper bits =head1 VERSION version 0.026 =for Pod::Coverage make_executable split_like_shell detildefy =head1 AUTHORS =over 4 =item * Ken Williams =item * Leon Timmermans =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2004 by Ken Williams, Leon Timmermans. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut EXTUTILS_HELPERS_UNIX $fatpacked{"ExtUtils/Helpers/VMS.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_HELPERS_VMS'; package ExtUtils::Helpers::VMS; $ExtUtils::Helpers::VMS::VERSION = '0.026'; use strict; use warnings FATAL => 'all'; use Exporter 5.57 'import'; our @EXPORT = qw/make_executable detildefy/; use File::Copy qw/copy/; sub make_executable { my $filename = shift; my $batchname = "$filename.com"; copy($filename, $batchname); ExtUtils::Helpers::Unix::make_executable($batchname); return; } sub detildefy { my $arg = shift; # Apparently double ~ are not translated. return $arg if ($arg =~ /^~~/); # Apparently ~ followed by whitespace are not translated. return $arg if ($arg =~ /^~ /); if ($arg =~ /^~/) { my $spec = $arg; # Remove the tilde $spec =~ s/^~//; # Remove any slash following the tilde if present. $spec =~ s#^/##; # break up the paths for the merge my $home = VMS::Filespec::unixify($ENV{HOME}); # In the default VMS mode, the trailing slash is present. # In Unix report mode it is not. The parsing logic assumes that # it is present. $home .= '/' unless $home =~ m#/$#; # Trivial case of just ~ by it self if ($spec eq '') { $home =~ s#/$##; return $home; } my ($hvol, $hdir, $hfile) = File::Spec::Unix->splitpath($home); if ($hdir eq '') { # Someone has tampered with $ENV{HOME} # So hfile is probably the directory since this should be # a path. $hdir = $hfile; } my ($vol, $dir, $file) = File::Spec::Unix->splitpath($spec); my @hdirs = File::Spec::Unix->splitdir($hdir); my @dirs = File::Spec::Unix->splitdir($dir); unless ($arg =~ m#^~/#) { # There is a home directory after the tilde, but it will already # be present in in @hdirs so we need to remove it by from @dirs. shift @dirs; } my $newdirs = File::Spec::Unix->catdir(@hdirs, @dirs); $arg = File::Spec::Unix->catpath($hvol, $newdirs, $file); } return $arg; } # ABSTRACT: VMS specific helper bits __END__ =pod =encoding UTF-8 =head1 NAME ExtUtils::Helpers::VMS - VMS specific helper bits =head1 VERSION version 0.026 =for Pod::Coverage make_executable detildefy =head1 AUTHORS =over 4 =item * Ken Williams =item * Leon Timmermans =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2004 by Ken Williams, Leon Timmermans. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut EXTUTILS_HELPERS_VMS $fatpacked{"ExtUtils/Helpers/Windows.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_HELPERS_WINDOWS'; package ExtUtils::Helpers::Windows; $ExtUtils::Helpers::Windows::VERSION = '0.026'; use strict; use warnings FATAL => 'all'; use Exporter 5.57 'import'; our @EXPORT = qw/make_executable detildefy/; use Config; use Carp qw/carp croak/; use ExtUtils::PL2Bat 'pl2bat'; sub make_executable { my $script = shift; if (-T $script && $script !~ / \. (?:bat|cmd) $ /x) { pl2bat(in => $script, update => 1); } return; } sub detildefy { my $value = shift; $value =~ s{ ^ ~ (?= [/\\] | $ ) }[$ENV{USERPROFILE}]x if $ENV{USERPROFILE}; return $value; } 1; # ABSTRACT: Windows specific helper bits __END__ =pod =encoding UTF-8 =head1 NAME ExtUtils::Helpers::Windows - Windows specific helper bits =head1 VERSION version 0.026 =for Pod::Coverage make_executable split_like_shell detildefy =head1 AUTHORS =over 4 =item * Ken Williams =item * Leon Timmermans =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2004 by Ken Williams, Leon Timmermans. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut EXTUTILS_HELPERS_WINDOWS $fatpacked{"ExtUtils/Install.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_INSTALL'; package ExtUtils::Install; use strict; use vars qw(@ISA @EXPORT $VERSION $MUST_REBOOT %Config); use AutoSplit; use Carp (); use Config qw(%Config); use Cwd qw(cwd); use Exporter; use ExtUtils::Packlist; use File::Basename qw(dirname); use File::Compare qw(compare); use File::Copy; use File::Find qw(find); use File::Path; use File::Spec; @ISA = ('Exporter'); @EXPORT = ('install','uninstall','pm_to_blib', 'install_default'); =pod =head1 NAME ExtUtils::Install - install files from here to there =head1 SYNOPSIS use ExtUtils::Install; install({ 'blib/lib' => 'some/install/dir' } ); uninstall($packlist); pm_to_blib({ 'lib/Foo/Bar.pm' => 'blib/lib/Foo/Bar.pm' }); =head1 VERSION 2.06 =cut $VERSION = '2.06'; # <-- do not forget to update the POD section just above this line! $VERSION = eval $VERSION; =pod =head1 DESCRIPTION Handles the installing and uninstalling of perl modules, scripts, man pages, etc... Both install() and uninstall() are specific to the way ExtUtils::MakeMaker handles the installation and deinstallation of perl modules. They are not designed as general purpose tools. On some operating systems such as Win32 installation may not be possible until after a reboot has occurred. This can have varying consequences: removing an old DLL does not impact programs using the new one, but if a new DLL cannot be installed properly until reboot then anything depending on it must wait. The package variable $ExtUtils::Install::MUST_REBOOT is used to store this status. If this variable is true then such an operation has occurred and anything depending on this module cannot proceed until a reboot has occurred. If this value is defined but false then such an operation has ocurred, but should not impact later operations. =over =begin _private =item _chmod($$;$) Wrapper to chmod() for debugging and error trapping. =item _warnonce(@) Warns about something only once. =item _choke(@) Dies with a special message. =back =end _private =cut my $Is_VMS = $^O eq 'VMS'; my $Is_MacPerl = $^O eq 'MacOS'; my $Is_Win32 = $^O eq 'MSWin32'; my $Is_cygwin = $^O eq 'cygwin'; my $CanMoveAtBoot = ($Is_Win32 || $Is_cygwin); my $Inc_uninstall_warn_handler; # install relative to here my $INSTALL_ROOT = $ENV{PERL_INSTALL_ROOT}; my $INSTALL_QUIET = $ENV{PERL_INSTALL_QUIET}; my $Curdir = File::Spec->curdir; my $Updir = File::Spec->updir; sub _estr(@) { return join "\n",'!' x 72,@_,'!' x 72,''; } {my %warned; sub _warnonce(@) { my $first=shift; my $msg=_estr "WARNING: $first",@_; warn $msg unless $warned{$msg}++; }} sub _choke(@) { my $first=shift; my $msg=_estr "ERROR: $first",@_; Carp::croak($msg); } sub _chmod($$;$) { my ( $mode, $item, $verbose )=@_; $verbose ||= 0; if (chmod $mode, $item) { printf "chmod(0%o, %s)\n",$mode, $item if $verbose > 1; } else { my $err="$!"; _warnonce sprintf "WARNING: Failed chmod(0%o, %s): %s\n", $mode, $item, $err if -e $item; } } =begin _private =over =item _move_file_at_boot( $file, $target, $moan ) OS-Specific, Win32/Cygwin Schedules a file to be moved/renamed/deleted at next boot. $file should be a filespec of an existing file $target should be a ref to an array if the file is to be deleted otherwise it should be a filespec for a rename. If the file is existing it will be replaced. Sets $MUST_REBOOT to 0 to indicate a deletion operation has occurred and sets it to 1 to indicate that a move operation has been requested. returns 1 on success, on failure if $moan is false errors are fatal. If $moan is true then returns 0 on error and warns instead of dies. =end _private =cut { my $Has_Win32API_File; sub _move_file_at_boot { #XXX OS-SPECIFIC my ( $file, $target, $moan )= @_; Carp::confess("Panic: Can't _move_file_at_boot on this platform!") unless $CanMoveAtBoot; my $descr= ref $target ? "'$file' for deletion" : "'$file' for installation as '$target'"; # *note* CanMoveAtBoot is only incidentally the same condition as below # this needs not hold true in the future. $Has_Win32API_File = ($Is_Win32 || $Is_cygwin) ? (eval {require Win32API::File; 1} || 0) : 0 unless defined $Has_Win32API_File; if ( ! $Has_Win32API_File ) { my @msg=( "Cannot schedule $descr at reboot.", "Try installing Win32API::File to allow operations on locked files", "to be scheduled during reboot. Or try to perform the operation by", "hand yourself. (You may need to close other perl processes first)" ); if ( $moan ) { _warnonce(@msg) } else { _choke(@msg) } return 0; } my $opts= Win32API::File::MOVEFILE_DELAY_UNTIL_REBOOT(); $opts= $opts | Win32API::File::MOVEFILE_REPLACE_EXISTING() unless ref $target; _chmod( 0666, $file ); _chmod( 0666, $target ) unless ref $target; if (Win32API::File::MoveFileEx( $file, $target, $opts )) { $MUST_REBOOT ||= ref $target ? 0 : 1; return 1; } else { my @msg=( "MoveFileEx $descr at reboot failed: $^E", "You may try to perform the operation by hand yourself. ", "(You may need to close other perl processes first).", ); if ( $moan ) { _warnonce(@msg) } else { _choke(@msg) } } return 0; } } =begin _private =item _unlink_or_rename( $file, $tryhard, $installing ) OS-Specific, Win32/Cygwin Tries to get a file out of the way by unlinking it or renaming it. On some OS'es (Win32 based) DLL files can end up locked such that they can be renamed but not deleted. Likewise sometimes a file can be locked such that it cant even be renamed or changed except at reboot. To handle these cases this routine finds a tempfile name that it can either rename the file out of the way or use as a proxy for the install so that the rename can happen later (at reboot). $file : the file to remove. $tryhard : should advanced tricks be used for deletion $installing : we are not merely deleting but we want to overwrite When $tryhard is not true if the unlink fails its fatal. When $tryhard is true then the file is attempted to be renamed. The renamed file is then scheduled for deletion. If the rename fails then $installing governs what happens. If it is false the failure is fatal. If it is true then an attempt is made to schedule installation at boot using a temporary file to hold the new file. If this fails then a fatal error is thrown, if it succeeds it returns the temporary file name (which will be a derivative of the original in the same directory) so that the caller can use it to install under. In all other cases of success returns $file. On failure throws a fatal error. =end _private =cut sub _unlink_or_rename { #XXX OS-SPECIFIC my ( $file, $tryhard, $installing )= @_; # this chmod was originally unconditional. However, its not needed on # POSIXy systems since permission to unlink a file is specified by the # directory rather than the file; and in fact it screwed up hard- and # symlinked files. Keep it for other platforms in case its still # needed there. if ($^O =~ /^(dos|os2|MSWin32|VMS)$/) { _chmod( 0666, $file ); } my $unlink_count = 0; while (unlink $file) { $unlink_count++; } return $file if $unlink_count > 0; my $error="$!"; _choke("Cannot unlink '$file': $!") unless $CanMoveAtBoot && $tryhard; my $tmp= "AAA"; ++$tmp while -e "$file.$tmp"; $tmp= "$file.$tmp"; warn "WARNING: Unable to unlink '$file': $error\n", "Going to try to rename it to '$tmp'.\n"; if ( rename $file, $tmp ) { warn "Rename successful. Scheduling '$tmp'\nfor deletion at reboot.\n"; # when $installing we can set $moan to true. # IOW, if we cant delete the renamed file at reboot its # not the end of the world. The other cases are more serious # and need to be fatal. _move_file_at_boot( $tmp, [], $installing ); return $file; } elsif ( $installing ) { _warnonce("Rename failed: $!. Scheduling '$tmp'\nfor". " installation as '$file' at reboot.\n"); _move_file_at_boot( $tmp, $file ); return $tmp; } else { _choke("Rename failed:$!", "Cannot proceed."); } } =pod =back =head2 Functions =begin _private =over =item _get_install_skip Handles loading the INSTALL.SKIP file. Returns an array of patterns to use. =cut sub _get_install_skip { my ( $skip, $verbose )= @_; if ($ENV{EU_INSTALL_IGNORE_SKIP}) { print "EU_INSTALL_IGNORE_SKIP is set, ignore skipfile settings\n" if $verbose>2; return []; } if ( ! defined $skip ) { print "Looking for install skip list\n" if $verbose>2; for my $file ( 'INSTALL.SKIP', $ENV{EU_INSTALL_SITE_SKIPFILE} ) { next unless $file; print "\tChecking for $file\n" if $verbose>2; if (-e $file) { $skip= $file; last; } } } if ($skip && !ref $skip) { print "Reading skip patterns from '$skip'.\n" if $verbose; if (open my $fh,$skip ) { my @patterns; while (<$fh>) { chomp; next if /^\s*(?:#|$)/; print "\tSkip pattern: $_\n" if $verbose>3; push @patterns, $_; } $skip= \@patterns; } else { warn "Can't read skip file:'$skip':$!\n"; $skip=[]; } } elsif ( UNIVERSAL::isa($skip,'ARRAY') ) { print "Using array for skip list\n" if $verbose>2; } elsif ($verbose) { print "No skip list found.\n" if $verbose>1; $skip= []; } warn "Got @{[0+@$skip]} skip patterns.\n" if $verbose>3; return $skip } =pod =item _have_write_access Abstract a -w check that tries to use POSIX::access() if possible. =cut { my $has_posix; sub _have_write_access { my $dir=shift; unless (defined $has_posix) { $has_posix= (!$Is_cygwin && !$Is_Win32 && eval 'local $^W; require POSIX; 1') || 0; } if ($has_posix) { return POSIX::access($dir, POSIX::W_OK()); } else { return -w $dir; } } } =pod =item _can_write_dir(C<$dir>) Checks whether a given directory is writable, taking account the possibility that the directory might not exist and would have to be created first. Returns a list, containing: C<($writable, $determined_by, @create)> C<$writable> says whether the directory is (hypothetically) writable C<$determined_by> is the directory the status was determined from. It will be either the C<$dir>, or one of its parents. C<@create> is a list of directories that would probably have to be created to make the requested directory. It may not actually be correct on relative paths with C<..> in them. But for our purposes it should work ok =cut sub _can_write_dir { my $dir=shift; return unless defined $dir and length $dir; my ($vol, $dirs, $file) = File::Spec->splitpath($dir,1); my @dirs = File::Spec->splitdir($dirs); unshift @dirs, File::Spec->curdir unless File::Spec->file_name_is_absolute($dir); my $path=''; my @make; while (@dirs) { if ($Is_VMS) { $dir = File::Spec->catdir($vol,@dirs); } else { $dir = File::Spec->catdir(@dirs); $dir = File::Spec->catpath($vol,$dir,'') if defined $vol and length $vol; } next if ( $dir eq $path ); if ( ! -e $dir ) { unshift @make,$dir; next; } if ( _have_write_access($dir) ) { return 1,$dir,@make } else { return 0,$dir,@make } } continue { pop @dirs; } return 0; } =pod =item _mkpath($dir,$show,$mode,$verbose,$dry_run) Wrapper around File::Path::mkpath() to handle errors. If $verbose is true and >1 then additional diagnostics will be produced, also this will force $show to true. If $dry_run is true then the directory will not be created but a check will be made to see whether it would be possible to write to the directory, or that it would be possible to create the directory. If $dry_run is not true dies if the directory can not be created or is not writable. =cut sub _mkpath { my ($dir,$show,$mode,$verbose,$dry_run)=@_; if ( $verbose && $verbose > 1 && ! -d $dir) { $show= 1; printf "mkpath(%s,%d,%#o)\n", $dir, $show, $mode; } if (!$dry_run) { if ( ! eval { File::Path::mkpath($dir,$show,$mode); 1 } ) { _choke("Can't create '$dir'","$@"); } } my ($can,$root,@make)=_can_write_dir($dir); if (!$can) { my @msg=( "Can't create '$dir'", $root ? "Do not have write permissions on '$root'" : "Unknown Error" ); if ($dry_run) { _warnonce @msg; } else { _choke @msg; } } elsif ($show and $dry_run) { print "$_\n" for @make; } } =pod =item _copy($from,$to,$verbose,$dry_run) Wrapper around File::Copy::copy to handle errors. If $verbose is true and >1 then additional diagnostics will be emitted. If $dry_run is true then the copy will not actually occur. Dies if the copy fails. =cut sub _copy { my ( $from, $to, $verbose, $dry_run)=@_; if ($verbose && $verbose>1) { printf "copy(%s,%s)\n", $from, $to; } if (!$dry_run) { File::Copy::copy($from,$to) or Carp::croak( _estr "ERROR: Cannot copy '$from' to '$to': $!" ); } } =pod =item _chdir($from) Wrapper around chdir to catch errors. If not called in void context returns the cwd from before the chdir. dies on error. =cut sub _chdir { my ($dir)= @_; my $ret; if (defined wantarray) { $ret= cwd; } chdir $dir or _choke("Couldn't chdir to '$dir': $!"); return $ret; } =pod =back =end _private =over =item B # deprecated forms install(\%from_to); install(\%from_to, $verbose, $dry_run, $uninstall_shadows, $skip, $always_copy, \%result); # recommended form as of 1.47 install([ from_to => \%from_to, verbose => 1, dry_run => 0, uninstall_shadows => 1, skip => undef, always_copy => 1, result => \%install_results, ]); Copies each directory tree of %from_to to its corresponding value preserving timestamps and permissions. There are two keys with a special meaning in the hash: "read" and "write". These contain packlist files. After the copying is done, install() will write the list of target files to $from_to{write}. If $from_to{read} is given the contents of this file will be merged into the written file. The read and the written file may be identical, but on AFS it is quite likely that people are installing to a different directory than the one where the files later appear. If $verbose is true, will print out each file removed. Default is false. This is "make install VERBINST=1". $verbose values going up to 5 show increasingly more diagnostics output. If $dry_run is true it will only print what it was going to do without actually doing it. Default is false. If $uninstall_shadows is true any differing versions throughout @INC will be uninstalled. This is "make install UNINST=1" As of 1.37_02 install() supports the use of a list of patterns to filter out files that shouldn't be installed. If $skip is omitted or undefined then install will try to read the list from INSTALL.SKIP in the CWD. This file is a list of regular expressions and is just like the MANIFEST.SKIP file used by L. A default site INSTALL.SKIP may be provided by setting then environment variable EU_INSTALL_SITE_SKIPFILE, this will only be used when there isn't a distribution specific INSTALL.SKIP. If the environment variable EU_INSTALL_IGNORE_SKIP is true then no install file filtering will be performed. If $skip is undefined then the skip file will be autodetected and used if it is found. If $skip is a reference to an array then it is assumed the array contains the list of patterns, if $skip is a true non reference it is assumed to be the filename holding the list of patterns, any other value of $skip is taken to mean that no install filtering should occur. B As of version 1.47 the following additions were made to the install interface. Note that the new argument style and use of the %result hash is recommended. The $always_copy parameter which when true causes files to be updated regardless as to whether they have changed, if it is defined but false then copies are made only if the files have changed, if it is undefined then the value of the environment variable EU_INSTALL_ALWAYS_COPY is used as default. The %result hash will be populated with the various keys/subhashes reflecting the install. Currently these keys and their structure are: install => { $target => $source }, install_fail => { $target => $source }, install_unchanged => { $target => $source }, install_filtered => { $source => $pattern }, uninstall => { $uninstalled => $source }, uninstall_fail => { $uninstalled => $source }, where C<$source> is the filespec of the file being installed. C<$target> is where it is being installed to, and C<$uninstalled> is any shadow file that is in C<@INC> or C<$ENV{PERL5LIB}> or other standard locations, and C<$pattern> is the pattern that caused a source file to be skipped. In future more keys will be added, such as to show created directories, however this requires changes in other modules and must therefore wait. These keys will be populated before any exceptions are thrown should there be an error. Note that all updates of the %result are additive, the hash will not be cleared before use, thus allowing status results of many installs to be easily aggregated. B If there is only one argument and it is a reference to an array then the array is assumed to contain a list of key-value pairs specifying the options. In this case the option "from_to" is mandatory. This style means that you do not have to supply a cryptic list of arguments and can use a self documenting argument list that is easier to understand. This is now the recommended interface to install(). B If all actions were successful install will return a hashref of the results as described above for the $result parameter. If any action is a failure then install will die, therefore it is recommended to pass in the $result parameter instead of using the return value. If the result parameter is provided then the returned hashref will be the passed in hashref. =cut sub install { #XXX OS-SPECIFIC my($from_to,$verbose,$dry_run,$uninstall_shadows,$skip,$always_copy,$result) = @_; if (@_==1 and eval { 1+@$from_to }) { my %opts = @$from_to; $from_to = $opts{from_to} or Carp::confess("from_to is a mandatory parameter"); $verbose = $opts{verbose}; $dry_run = $opts{dry_run}; $uninstall_shadows = $opts{uninstall_shadows}; $skip = $opts{skip}; $always_copy = $opts{always_copy}; $result = $opts{result}; } $result ||= {}; $verbose ||= 0; $dry_run ||= 0; $skip= _get_install_skip($skip,$verbose); $always_copy = $ENV{EU_INSTALL_ALWAYS_COPY} || $ENV{EU_ALWAYS_COPY} || 0 unless defined $always_copy; my(%from_to) = %$from_to; my(%pack, $dir, %warned); my($packlist) = ExtUtils::Packlist->new(); local(*DIR); for (qw/read write/) { $pack{$_}=$from_to{$_}; delete $from_to{$_}; } my $tmpfile = install_rooted_file($pack{"read"}); $packlist->read($tmpfile) if (-f $tmpfile); my $cwd = cwd(); my @found_files; my %check_dirs; MOD_INSTALL: foreach my $source (sort keys %from_to) { #copy the tree to the target directory without altering #timestamp and permission and remember for the .packlist #file. The packlist file contains the absolute paths of the #install locations. AFS users may call this a bug. We'll have #to reconsider how to add the means to satisfy AFS users also. #October 1997: we want to install .pm files into archlib if #there are any files in arch. So we depend on having ./blib/arch #hardcoded here. my $targetroot = install_rooted_dir($from_to{$source}); my $blib_lib = File::Spec->catdir('blib', 'lib'); my $blib_arch = File::Spec->catdir('blib', 'arch'); if ($source eq $blib_lib and exists $from_to{$blib_arch} and directory_not_empty($blib_arch) ){ $targetroot = install_rooted_dir($from_to{$blib_arch}); print "Files found in $blib_arch: installing files in $blib_lib into architecture dependent library tree\n"; } next unless -d $source; _chdir($source); # 5.5.3's File::Find missing no_chdir option # XXX OS-SPECIFIC # File::Find seems to always be Unixy except on MacPerl :( my $current_directory= $Is_MacPerl ? $Curdir : '.'; find(sub { my ($mode,$size,$atime,$mtime) = (stat)[2,7,8,9]; return if !-f _; my $origfile = $_; return if $origfile eq ".exists"; my $targetdir = File::Spec->catdir($targetroot, $File::Find::dir); my $targetfile = File::Spec->catfile($targetdir, $origfile); my $sourcedir = File::Spec->catdir($source, $File::Find::dir); my $sourcefile = File::Spec->catfile($sourcedir, $origfile); for my $pat (@$skip) { if ( $sourcefile=~/$pat/ ) { print "Skipping $targetfile (filtered)\n" if $verbose>1; $result->{install_filtered}{$sourcefile} = $pat; return; } } # we have to do this for back compat with old File::Finds # and because the target is relative my $save_cwd = _chdir($cwd); my $diff = 0; # XXX: I wonder how useful this logic is actually -- demerphq if ( $always_copy or !-f $targetfile or -s $targetfile != $size) { $diff++; } else { # we might not need to copy this file $diff = compare($sourcefile, $targetfile); } $check_dirs{$targetdir}++ unless -w $targetfile; push @found_files, [ $diff, $File::Find::dir, $origfile, $mode, $size, $atime, $mtime, $targetdir, $targetfile, $sourcedir, $sourcefile, ]; #restore the original directory we were in when File::Find #called us so that it doesn't get horribly confused. _chdir($save_cwd); }, $current_directory ); _chdir($cwd); } foreach my $targetdir (sort keys %check_dirs) { _mkpath( $targetdir, 0, 0755, $verbose, $dry_run ); } foreach my $found (@found_files) { my ($diff, $ffd, $origfile, $mode, $size, $atime, $mtime, $targetdir, $targetfile, $sourcedir, $sourcefile)= @$found; my $realtarget= $targetfile; if ($diff) { eval { if (-f $targetfile) { print "_unlink_or_rename($targetfile)\n" if $verbose>1; $targetfile= _unlink_or_rename( $targetfile, 'tryhard', 'install' ) unless $dry_run; } elsif ( ! -d $targetdir ) { _mkpath( $targetdir, 0, 0755, $verbose, $dry_run ); } print "Installing $targetfile\n"; _copy( $sourcefile, $targetfile, $verbose, $dry_run, ); #XXX OS-SPECIFIC print "utime($atime,$mtime,$targetfile)\n" if $verbose>1; utime($atime,$mtime + $Is_VMS,$targetfile) unless $dry_run>1; $mode = 0444 | ( $mode & 0111 ? 0111 : 0 ); $mode = $mode | 0222 if $realtarget ne $targetfile; _chmod( $mode, $targetfile, $verbose ); $result->{install}{$targetfile} = $sourcefile; 1 } or do { $result->{install_fail}{$targetfile} = $sourcefile; die $@; }; } else { $result->{install_unchanged}{$targetfile} = $sourcefile; print "Skipping $targetfile (unchanged)\n" if $verbose; } if ( $uninstall_shadows ) { inc_uninstall($sourcefile,$ffd, $verbose, $dry_run, $realtarget ne $targetfile ? $realtarget : "", $result); } # Record the full pathname. $packlist->{$targetfile}++; } if ($pack{'write'}) { $dir = install_rooted_dir(dirname($pack{'write'})); _mkpath( $dir, 0, 0755, $verbose, $dry_run ); print "Writing $pack{'write'}\n" if $verbose; $packlist->write(install_rooted_file($pack{'write'})) unless $dry_run; } _do_cleanup($verbose); return $result; } =begin _private =item _do_cleanup Standardize finish event for after another instruction has occurred. Handles converting $MUST_REBOOT to a die for instance. =end _private =cut sub _do_cleanup { my ($verbose) = @_; if ($MUST_REBOOT) { die _estr "Operation not completed! ", "You must reboot to complete the installation.", "Sorry."; } elsif (defined $MUST_REBOOT & $verbose) { warn _estr "Installation will be completed at the next reboot.\n", "However it is not necessary to reboot immediately.\n"; } } =begin _undocumented =item install_rooted_file( $file ) Returns $file, or catfile($INSTALL_ROOT,$file) if $INSTALL_ROOT is defined. =item install_rooted_dir( $dir ) Returns $dir, or catdir($INSTALL_ROOT,$dir) if $INSTALL_ROOT is defined. =end _undocumented =cut sub install_rooted_file { if (defined $INSTALL_ROOT) { File::Spec->catfile($INSTALL_ROOT, $_[0]); } else { $_[0]; } } sub install_rooted_dir { if (defined $INSTALL_ROOT) { File::Spec->catdir($INSTALL_ROOT, $_[0]); } else { $_[0]; } } =begin _undocumented =item forceunlink( $file, $tryhard ) Tries to delete a file. If $tryhard is true then we will use whatever devious tricks we can to delete the file. Currently this only applies to Win32 in that it will try to use Win32API::File to schedule a delete at reboot. A wrapper for _unlink_or_rename(). =end _undocumented =cut sub forceunlink { my ( $file, $tryhard )= @_; #XXX OS-SPECIFIC _unlink_or_rename( $file, $tryhard, not("installing") ); } =begin _undocumented =item directory_not_empty( $dir ) Returns 1 if there is an .exists file somewhere in a directory tree. Returns 0 if there is not. =end _undocumented =cut sub directory_not_empty ($) { my($dir) = @_; my $files = 0; find(sub { return if $_ eq ".exists"; if (-f) { $File::Find::prune++; $files = 1; } }, $dir); return $files; } =pod =item B I install_default(); install_default($fullext); Calls install() with arguments to copy a module from blib/ to the default site installation location. $fullext is the name of the module converted to a directory (ie. Foo::Bar would be Foo/Bar). If $fullext is not specified, it will attempt to read it from @ARGV. This is primarily useful for install scripts. B This function is not really useful because of the hard-coded install location with no way to control site vs core vs vendor directories and the strange way in which the module name is given. Consider its use discouraged. =cut sub install_default { @_ < 2 or Carp::croak("install_default should be called with 0 or 1 argument"); my $FULLEXT = @_ ? shift : $ARGV[0]; defined $FULLEXT or die "Do not know to where to write install log"; my $INST_LIB = File::Spec->catdir($Curdir,"blib","lib"); my $INST_ARCHLIB = File::Spec->catdir($Curdir,"blib","arch"); my $INST_BIN = File::Spec->catdir($Curdir,'blib','bin'); my $INST_SCRIPT = File::Spec->catdir($Curdir,'blib','script'); my $INST_MAN1DIR = File::Spec->catdir($Curdir,'blib','man1'); my $INST_MAN3DIR = File::Spec->catdir($Curdir,'blib','man3'); my @INST_HTML; if($Config{installhtmldir}) { my $INST_HTMLDIR = File::Spec->catdir($Curdir,'blib','html'); @INST_HTML = ($INST_HTMLDIR => $Config{installhtmldir}); } install({ read => "$Config{sitearchexp}/auto/$FULLEXT/.packlist", write => "$Config{installsitearch}/auto/$FULLEXT/.packlist", $INST_LIB => (directory_not_empty($INST_ARCHLIB)) ? $Config{installsitearch} : $Config{installsitelib}, $INST_ARCHLIB => $Config{installsitearch}, $INST_BIN => $Config{installbin} , $INST_SCRIPT => $Config{installscript}, $INST_MAN1DIR => $Config{installman1dir}, $INST_MAN3DIR => $Config{installman3dir}, @INST_HTML, },1,0,0); } =item B uninstall($packlist_file); uninstall($packlist_file, $verbose, $dont_execute); Removes the files listed in a $packlist_file. If $verbose is true, will print out each file removed. Default is false. If $dont_execute is true it will only print what it was going to do without actually doing it. Default is false. =cut sub uninstall { my($fil,$verbose,$dry_run) = @_; $verbose ||= 0; $dry_run ||= 0; die _estr "ERROR: no packlist file found: '$fil'" unless -f $fil; # my $my_req = $self->catfile(qw(auto ExtUtils Install forceunlink.al)); # require $my_req; # Hairy, but for the first my ($packlist) = ExtUtils::Packlist->new($fil); foreach (sort(keys(%$packlist))) { chomp; print "unlink $_\n" if $verbose; forceunlink($_,'tryhard') unless $dry_run; } print "unlink $fil\n" if $verbose; forceunlink($fil, 'tryhard') unless $dry_run; _do_cleanup($verbose); } =begin _undocumented =item inc_uninstall($filepath,$libdir,$verbose,$dry_run,$ignore,$results) Remove shadowed files. If $ignore is true then it is assumed to hold a filename to ignore. This is used to prevent spurious warnings from occurring when doing an install at reboot. We now only die when failing to remove a file that has precedence over our own, when our install has precedence we only warn. $results is assumed to contain a hashref which will have the keys 'uninstall' and 'uninstall_fail' populated with keys for the files removed and values of the source files they would shadow. =end _undocumented =cut sub inc_uninstall { my($filepath,$libdir,$verbose,$dry_run,$ignore,$results) = @_; my($dir); $ignore||=""; my $file = (File::Spec->splitpath($filepath))[2]; my %seen_dir = (); my @PERL_ENV_LIB = split $Config{path_sep}, defined $ENV{'PERL5LIB'} ? $ENV{'PERL5LIB'} : $ENV{'PERLLIB'} || ''; my @dirs=( @PERL_ENV_LIB, @INC, @Config{qw(archlibexp privlibexp sitearchexp sitelibexp)}); #warn join "\n","---",@dirs,"---"; my $seen_ours; foreach $dir ( @dirs ) { my $canonpath = $Is_VMS ? $dir : File::Spec->canonpath($dir); next if $canonpath eq $Curdir; next if $seen_dir{$canonpath}++; my $targetfile = File::Spec->catfile($canonpath,$libdir,$file); next unless -f $targetfile; # The reason why we compare file's contents is, that we cannot # know, which is the file we just installed (AFS). So we leave # an identical file in place my $diff = 0; if ( -f $targetfile && -s _ == -s $filepath) { # We have a good chance, we can skip this one $diff = compare($filepath,$targetfile); } else { $diff++; } print "#$file and $targetfile differ\n" if $diff && $verbose > 1; if (!$diff or $targetfile eq $ignore) { $seen_ours = 1; next; } if ($dry_run) { $results->{uninstall}{$targetfile} = $filepath; if ($verbose) { $Inc_uninstall_warn_handler ||= ExtUtils::Install::Warn->new(); $libdir =~ s|^\./||s ; # That's just cosmetics, no need to port. It looks prettier. $Inc_uninstall_warn_handler->add( File::Spec->catfile($libdir, $file), $targetfile ); } # if not verbose, we just say nothing } else { print "Unlinking $targetfile (shadowing?)\n" if $verbose; eval { die "Fake die for testing" if $ExtUtils::Install::Testing and ucase(File::Spec->canonpath($ExtUtils::Install::Testing)) eq ucase($targetfile); forceunlink($targetfile,'tryhard'); $results->{uninstall}{$targetfile} = $filepath; 1; } or do { $results->{fail_uninstall}{$targetfile} = $filepath; if ($seen_ours) { warn "Failed to remove probably harmless shadow file '$targetfile'\n"; } else { die "$@\n"; } }; } } } =begin _undocumented =item run_filter($cmd,$src,$dest) Filter $src using $cmd into $dest. =end _undocumented =cut sub run_filter { my ($cmd, $src, $dest) = @_; local(*CMD, *SRC); open(CMD, "|$cmd >$dest") || die "Cannot fork: $!"; open(SRC, $src) || die "Cannot open $src: $!"; my $buf; my $sz = 1024; while (my $len = sysread(SRC, $buf, $sz)) { syswrite(CMD, $buf, $len); } close SRC; close CMD or die "Filter command '$cmd' failed for $src"; } =pod =item B pm_to_blib(\%from_to); pm_to_blib(\%from_to, $autosplit_dir); pm_to_blib(\%from_to, $autosplit_dir, $filter_cmd); Copies each key of %from_to to its corresponding value efficiently. If an $autosplit_dir is provided, all .pm files will be autosplit into it. Any destination directories are created. $filter_cmd is an optional shell command to run each .pm file through prior to splitting and copying. Input is the contents of the module, output the new module contents. You can have an environment variable PERL_INSTALL_ROOT set which will be prepended as a directory to each installed file (and directory). By default verbose output is generated, setting the PERL_INSTALL_QUIET environment variable will silence this output. =cut sub pm_to_blib { my($fromto,$autodir,$pm_filter) = @_; _mkpath($autodir,0,0755) if defined $autodir; while(my($from, $to) = each %$fromto) { if( -f $to && -s $from == -s $to && -M $to < -M $from ) { print "Skip $to (unchanged)\n" unless $INSTALL_QUIET; next; } # When a pm_filter is defined, we need to pre-process the source first # to determine whether it has changed or not. Therefore, only perform # the comparison check when there's no filter to be ran. # -- RAM, 03/01/2001 my $need_filtering = defined $pm_filter && length $pm_filter && $from =~ /\.pm$/; if (!$need_filtering && 0 == compare($from,$to)) { print "Skip $to (unchanged)\n" unless $INSTALL_QUIET; next; } if (-f $to){ # we wont try hard here. its too likely to mess things up. forceunlink($to); } else { _mkpath(dirname($to),0,0755); } if ($need_filtering) { run_filter($pm_filter, $from, $to); print "$pm_filter <$from >$to\n"; } else { _copy( $from, $to ); print "cp $from $to\n" unless $INSTALL_QUIET; } my($mode,$atime,$mtime) = (stat $from)[2,8,9]; utime($atime,$mtime+$Is_VMS,$to); _chmod(0444 | ( $mode & 0111 ? 0111 : 0 ),$to); next unless $from =~ /\.pm$/; _autosplit($to,$autodir) if defined $autodir; } } =begin _private =item _autosplit From 1.0307 back, AutoSplit will sometimes leave an open filehandle to the file being split. This causes problems on systems with mandatory locking (ie. Windows). So we wrap it and close the filehandle. =end _private =cut sub _autosplit { #XXX OS-SPECIFIC my $retval = autosplit(@_); close *AutoSplit::IN if defined *AutoSplit::IN{IO}; return $retval; } package ExtUtils::Install::Warn; sub new { bless {}, shift } sub add { my($self,$file,$targetfile) = @_; push @{$self->{$file}}, $targetfile; } sub DESTROY { unless(defined $INSTALL_ROOT) { my $self = shift; my($file,$i,$plural); foreach $file (sort keys %$self) { $plural = @{$self->{$file}} > 1 ? "s" : ""; print "## Differing version$plural of $file found. You might like to\n"; for (0..$#{$self->{$file}}) { print "rm ", $self->{$file}[$_], "\n"; $i++; } } $plural = $i>1 ? "all those files" : "this file"; my $inst = (_invokant() eq 'ExtUtils::MakeMaker') ? ( $Config::Config{make} || 'make' ).' install' . ( $Is_VMS ? '/MACRO="UNINST"=1' : ' UNINST=1' ) : './Build install uninst=1'; print "## Running '$inst' will unlink $plural for you.\n"; } } =begin _private =item _invokant Does a heuristic on the stack to see who called us for more intelligent error messages. Currently assumes we will be called only by Module::Build or by ExtUtils::MakeMaker. =end _private =cut sub _invokant { my @stack; my $frame = 0; while (my $file = (caller($frame++))[1]) { push @stack, (File::Spec->splitpath($file))[2]; } my $builder; my $top = pop @stack; if ($top =~ /^Build/i || exists($INC{'Module/Build.pm'})) { $builder = 'Module::Build'; } else { $builder = 'ExtUtils::MakeMaker'; } return $builder; } =pod =back =head1 ENVIRONMENT =over 4 =item B Will be prepended to each install path. =item B Will prevent the automatic use of INSTALL.SKIP as the install skip file. =item B If there is no INSTALL.SKIP file in the make directory then this value can be used to provide a default. =item B If this environment variable is true then normal install processes will always overwrite older identical files during the install process. Note that the alias EU_ALWAYS_COPY will be supported if EU_INSTALL_ALWAYS_COPY is not defined until at least the 1.50 release. Please ensure you use the correct EU_INSTALL_ALWAYS_COPY. =back =head1 AUTHOR Original author lost in the mists of time. Probably the same as Makemaker. Production release currently maintained by demerphq C, extensive changes by Michael G. Schwern. Send bug reports via http://rt.cpan.org/. Please send your generated Makefile along with your report. =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L =cut 1; EXTUTILS_INSTALL $fatpacked{"ExtUtils/InstallPaths.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'EXTUTILS_INSTALLPATHS'; package ExtUtils::InstallPaths; $ExtUtils::InstallPaths::VERSION = '0.012'; use 5.006; use strict; use warnings; use File::Spec (); use Carp (); use ExtUtils::Config 0.002; my %complex_accessors = map { $_ => 1 } qw/prefix_relpaths install_sets/; my %hash_accessors = map { $_ => 1 } qw/install_path install_base_relpaths original_prefix /; my %defaults = ( installdirs => 'site', install_base => undef, prefix => undef, verbose => 0, create_packlist => 1, dist_name => undef, module_name => undef, destdir => undef, install_path => sub { {} }, install_sets => \&_default_install_sets, original_prefix => \&_default_original_prefix, install_base_relpaths => \&_default_base_relpaths, prefix_relpaths => \&_default_prefix_relpaths, ); sub _merge_shallow { my ($name, $filter) = @_; return sub { my ($override, $config) = @_; my $defaults = $defaults{$name}->($config); $filter->($_) for grep $filter, values %$override; return { %$defaults, %$override }; } } sub _merge_deep { my ($name, $filter) = @_; return sub { my ($override, $config) = @_; my $defaults = $defaults{$name}->($config); my $pair_for = sub { my $key = shift; my %override = %{ $override->{$key} || {} }; $filter && $filter->($_) for values %override; return $key => { %{ $defaults->{$key} }, %override }; }; return { map { $pair_for->($_) } keys %$defaults }; } } my %allowed_installdir = map { $_ => 1 } qw/core site vendor/; my $must_be_relative = sub { Carp::croak('Value must be a relative path') if File::Spec->file_name_is_absolute($_[0]) }; my %deep_filter = map { $_ => $must_be_relative } qw/install_base_relpaths prefix_relpaths/; my %filter = ( installdirs => sub { my $value = shift; $value = 'core', Carp::carp('Perhaps you meant installdirs to be "core" rather than "perl"?') if $value eq 'perl'; Carp::croak('installdirs must be one of "core", "site", or "vendor"') if not $allowed_installdir{$value}; return $value; }, (map { $_ => _merge_shallow($_, $deep_filter{$_}) } qw/original_prefix install_base_relpaths/), (map { $_ => _merge_deep($_, $deep_filter{$_}) } qw/install_sets prefix_relpaths/), ); sub new { my ($class, %args) = @_; my $config = $args{config} || ExtUtils::Config->new; my %self = ( config => $config, map { $_ => exists $args{$_} ? $filter{$_} ? $filter{$_}->($args{$_}, $config) : $args{$_} : ref $defaults{$_} ? $defaults{$_}->($config) : $defaults{$_} } keys %defaults, ); $self{module_name} ||= do { my $module_name = $self{dist_name}; $module_name =~ s/-/::/g; $module_name } if defined $self{dist_name}; return bless \%self, $class; } for my $attribute (keys %defaults) { no strict qw/refs/; *{$attribute} = $hash_accessors{$attribute} ? sub { my ($self, $key) = @_; Carp::confess("$attribute needs key") if not defined $key; return $self->{$attribute}{$key}; } : $complex_accessors{$attribute} ? sub { my ($self, $installdirs, $key) = @_; Carp::confess("$attribute needs installdir") if not defined $installdirs; Carp::confess("$attribute needs key") if not defined $key; return $self->{$attribute}{$installdirs}{$key}; } : sub { my $self = shift; return $self->{$attribute}; }; } my $script = $] > 5.008000 ? 'script' : 'bin'; my @install_sets_keys = qw/lib arch bin script bindoc libdoc binhtml libhtml/; my @install_sets_tail = ('bin', $script, qw/man1dir man3dir html1dir html3dir/); my %install_sets_values = ( core => [ qw/privlib archlib /, @install_sets_tail ], site => [ map { "site$_" } qw/lib arch/, @install_sets_tail ], vendor => [ map { "vendor$_" } qw/lib arch/, @install_sets_tail ], ); sub _default_install_sets { my $c = shift; my %ret; for my $installdir (qw/core site vendor/) { @{$ret{$installdir}}{@install_sets_keys} = map { $c->get("install$_") } @{ $install_sets_values{$installdir} }; } return \%ret; } sub _default_base_relpaths { my $config = shift; return { lib => ['lib', 'perl5'], arch => ['lib', 'perl5', $config->get('archname')], bin => ['bin'], script => ['bin'], bindoc => ['man', 'man1'], libdoc => ['man', 'man3'], binhtml => ['html'], libhtml => ['html'], }; } my %common_prefix_relpaths = ( bin => ['bin'], script => ['bin'], bindoc => ['man', 'man1'], libdoc => ['man', 'man3'], binhtml => ['html'], libhtml => ['html'], ); sub _default_prefix_relpaths { my $c = shift; my @libstyle = $c->get('installstyle') ? File::Spec->splitdir($c->get('installstyle')) : qw(lib perl5); my $arch = $c->get('archname'); my $version = $c->get('version'); return { core => { lib => [@libstyle], arch => [@libstyle, $version, $arch], %common_prefix_relpaths, }, vendor => { lib => [@libstyle], arch => [@libstyle, $version, $arch], %common_prefix_relpaths, }, site => { lib => [@libstyle, 'site_perl'], arch => [@libstyle, 'site_perl', $version, $arch], %common_prefix_relpaths, }, }; } sub _default_original_prefix { my $c = shift; my %ret = ( core => $c->get('installprefixexp'), site => $c->get('siteprefixexp'), vendor => $c->get('usevendorprefix') ? $c->get('vendorprefixexp') : '', ); return \%ret; } sub _log_verbose { my $self = shift; print @_ if $self->verbose; return; } # Given a file type, will return true if the file type would normally # be installed when neither install-base nor prefix has been set. # I.e. it will be true only if the path is set from Config.pm or # set explicitly by the user via install-path. sub is_default_installable { my $self = shift; my $type = shift; my $installable = $self->install_destination($type) && ( $self->install_path($type) || $self->install_sets($self->installdirs, $type)); return $installable ? 1 : 0; } sub _prefixify_default { my $self = shift; my $type = shift; my $rprefix = shift; my $default = $self->prefix_relpaths($self->installdirs, $type); if( !$default ) { $self->_log_verbose(" no default install location for type '$type', using prefix '$rprefix'.\n"); return $rprefix; } else { return File::Spec->catdir(@{$default}); } } # Translated from ExtUtils::MM_Unix::prefixify() sub _prefixify_novms { my($self, $path, $sprefix, $type) = @_; my $rprefix = $self->prefix; $rprefix .= '/' if $sprefix =~ m{/$}; $self->_log_verbose(" prefixify $path from $sprefix to $rprefix\n") if defined $path && length $path; if (not defined $path or length $path == 0 ) { $self->_log_verbose(" no path to prefixify, falling back to default.\n"); return $self->_prefixify_default( $type, $rprefix ); } elsif( !File::Spec->file_name_is_absolute($path) ) { $self->_log_verbose(" path is relative, not prefixifying.\n"); } elsif( $path !~ s{^\Q$sprefix\E\b}{}s ) { $self->_log_verbose(" cannot prefixify, falling back to default.\n"); return $self->_prefixify_default( $type, $rprefix ); } $self->_log_verbose(" now $path in $rprefix\n"); return $path; } sub _catprefix_vms { my ($self, $rprefix, $default) = @_; my ($rvol, $rdirs) = File::Spec->splitpath($rprefix); if ($rvol) { return File::Spec->catpath($rvol, File::Spec->catdir($rdirs, $default), ''); } else { return File::Spec->catdir($rdirs, $default); } } sub _prefixify_vms { my($self, $path, $sprefix, $type) = @_; my $rprefix = $self->prefix; return '' unless defined $path; $self->_log_verbose(" prefixify $path from $sprefix to $rprefix\n"); require VMS::Filespec; # Translate $(PERLPREFIX) to a real path. $rprefix = VMS::Filespec::vmspath($rprefix) if $rprefix; $sprefix = VMS::Filespec::vmspath($sprefix) if $sprefix; $self->_log_verbose(" rprefix translated to $rprefix\n sprefix translated to $sprefix\n"); if (length($path) == 0 ) { $self->_log_verbose(" no path to prefixify.\n") } elsif (!File::Spec->file_name_is_absolute($path)) { $self->_log_verbose(" path is relative, not prefixifying.\n"); } elsif ($sprefix eq $rprefix) { $self->_log_verbose(" no new prefix.\n"); } else { my ($path_vol, $path_dirs) = File::Spec->splitpath( $path ); my $vms_prefix = $self->config->get('vms_prefix'); if ($path_vol eq $vms_prefix.':') { $self->_log_verbose(" $vms_prefix: seen\n"); $path_dirs =~ s{^\[}{\[.} unless $path_dirs =~ m{^\[\.}; $path = $self->_catprefix_vms($rprefix, $path_dirs); } else { $self->_log_verbose(" cannot prefixify.\n"); return File::Spec->catdir($self->prefix_relpaths($self->installdirs, $type)); } } $self->_log_verbose(" now $path\n"); return $path; } BEGIN { *_prefixify = $^O eq 'VMS' ? \&_prefixify_vms : \&_prefixify_novms } # Translated from ExtUtils::MM_Any::init_INSTALL_from_PREFIX sub prefix_relative { my ($self, $installdirs, $type) = @_; my $relpath = $self->install_sets($installdirs, $type); return $self->_prefixify($relpath, $self->original_prefix($installdirs), $type); } sub install_destination { my ($self, $type) = @_; return $self->install_path($type) if $self->install_path($type); if ( $self->install_base ) { my $relpath = $self->install_base_relpaths($type); return $relpath ? File::Spec->catdir($self->install_base, @{$relpath}) : undef; } if ( $self->prefix ) { my $relpath = $self->prefix_relative($self->installdirs, $type); return $relpath ? File::Spec->catdir($self->prefix, $relpath) : undef; } return $self->install_sets($self->installdirs, $type); } sub install_types { my $self = shift; my %types = ( %{ $self->{install_path} }, $self->install_base ? %{ $self->{install_base_relpaths} } : $self->prefix ? %{ $self->{prefix_relpaths}{ $self->installdirs } } : %{ $self->{install_sets}{ $self->installdirs } }); return sort keys %types; } sub install_map { my ($self, $dirs) = @_; my %localdir_for; if ($dirs && %$dirs) { %localdir_for = %$dirs; } else { foreach my $type ($self->install_types) { $localdir_for{$type} = File::Spec->catdir('blib', $type); } } my (%map, @skipping); foreach my $type (keys %localdir_for) { next if not -e $localdir_for{$type}; if (my $dest = $self->install_destination($type)) { $map{$localdir_for{$type}} = $dest; } else { push @skipping, $type; } } warn "WARNING: Can't figure out install path for types: @skipping\nFiles will not be installed.\n" if @skipping; # Write the packlist into the same place as ExtUtils::MakeMaker. if ($self->create_packlist and my $module_name = $self->module_name) { my $archdir = $self->install_destination('arch'); my @ext = split /::/, $module_name; $map{write} = File::Spec->catfile($archdir, 'auto', @ext, '.packlist'); } # Handle destdir if (length(my $destdir = $self->destdir || '')) { foreach (keys %map) { # Need to remove volume from $map{$_} using splitpath, or else # we'll create something crazy like C:\Foo\Bar\E:\Baz\Quux # VMS will always have the file separate than the path. my ($volume, $path, $file) = File::Spec->splitpath( $map{$_}, 0 ); # catdir needs a list of directories, or it will create something # crazy like volume:[Foo.Bar.volume.Baz.Quux] my @dirs = File::Spec->splitdir($path); # First merge the directories $path = File::Spec->catdir($destdir, @dirs); # Then put the file back on if there is one. if ($file ne '') { $map{$_} = File::Spec->catfile($path, $file) } else { $map{$_} = $path; } } } $map{read} = ''; # To keep ExtUtils::Install quiet return \%map; } 1; # ABSTRACT: Build.PL install path logic made easy __END__ =pod =encoding UTF-8 =head1 NAME ExtUtils::InstallPaths - Build.PL install path logic made easy =head1 VERSION version 0.012 =head1 SYNOPSIS use ExtUtils::InstallPaths; use ExtUtils::Install 'install'; GetOptions(\my %opt, 'install_base=s', 'install_path=s%', 'installdirs=s', 'destdir=s', 'prefix=s', 'uninst:1', 'verbose:1'); my $paths = ExtUtils::InstallPaths->new(%opt, dist_name => $dist_name); install($paths->install_map, $opt{verbose}, 0, $opt{uninst}); =head1 DESCRIPTION This module tries to make install path resolution as easy as possible. When you want to install a module, it needs to figure out where to install things. The nutshell version of how this works is that default installation locations are determined from L, and they may be individually overridden by using the C attribute. An C attribute lets you specify an alternative installation root like F and C does something similar in a rather different (and more complicated) way. C lets you specify a temporary installation directory like F in case you want to create bundled-up installable packages. The following types are supported by default. =over 4 =item * lib Usually pure-Perl module files ending in F<.pm> or F<.pod>. =item * arch "Architecture-dependent" module files, usually produced by compiling XS, L, or similar code. =item * script Programs written in pure Perl. In order to improve reuse, you may want to make these as small as possible - put the code into modules whenever possible. =item * bin "Architecture-dependent" executable programs, i.e. compiled C code or something. Pretty rare to see this in a perl distribution, but it happens. =item * bindoc Documentation for the stuff in C >> in a JSON text, with the cost of bloating the size of JSON texts. This option may be useful when you embed JSON in HTML, but embedding arbitrary JSON in HTML (by some HTML template toolkit or by string interpolation) is risky in general. You must escape necessary characters in correct order, depending on the context. C will not be affected in any way. =head2 indent_length $json = $json->indent_length($number_of_spaces) $length = $json->get_indent_length This option is only useful when you also enable C or C. JSON::XS indents with three spaces when you C (if requested by C or C), and the number cannot be changed. JSON::PP allows you to change/get the number of indent spaces with these mutator/accessor. The default number of spaces is three (the same as JSON::XS), and the acceptable range is from C<0> (no indentation; it'd be better to disable indentation by C) to C<15>. =head2 sort_by $json = $json->sort_by($code_ref) $json = $json->sort_by($subroutine_name) If you just want to sort keys (names) in JSON objects when you C, enable C option (see above) that allows you to sort object keys alphabetically. If you do need to sort non-alphabetically for whatever reasons, you can give a code reference (or a subroutine name) to C, then the argument will be passed to Perl's C built-in function. As the sorting is done in the JSON::PP scope, you usually need to prepend C to the subroutine name, and the special variables C<$a> and C<$b> used in the subrontine used by C function. Example: my %ORDER = (id => 1, class => 2, name => 3); $json->sort_by(sub { ($ORDER{$JSON::PP::a} // 999) <=> ($ORDER{$JSON::PP::b} // 999) or $JSON::PP::a cmp $JSON::PP::b }); print $json->encode([ {name => 'CPAN', id => 1, href => 'http://cpan.org'} ]); # [{"id":1,"name":"CPAN","href":"http://cpan.org"}] Note that C affects all the plain hashes in the data structure. If you need finer control, C necessary hashes with a module that implements ordered hash (such as L and L). C and C don't affect the key order in Cd hashes. use Hash::Ordered; tie my %hash, 'Hash::Ordered', (name => 'CPAN', id => 1, href => 'http://cpan.org'); print $json->encode([\%hash]); # [{"name":"CPAN","id":1,"href":"http://cpan.org"}] # order is kept =head1 INCREMENTAL PARSING This section is also taken from JSON::XS. In some cases, there is the need for incremental parsing of JSON texts. While this module always has to keep both JSON text and resulting Perl data structure in memory at one time, it does allow you to parse a JSON stream incrementally. It does so by accumulating text until it has a full JSON object, which it then can decode. This process is similar to using C to see if a full JSON object is available, but is much more efficient (and can be implemented with a minimum of method calls). JSON::PP will only attempt to parse the JSON text once it is sure it has enough text to get a decisive result, using a very simple but truly incremental parser. This means that it sometimes won't stop as early as the full parser, for example, it doesn't detect mismatched parentheses. The only thing it guarantees is that it starts decoding as soon as a syntactically valid JSON text has been seen. This means you need to set resource limits (e.g. C) to ensure the parser will stop parsing in the presence if syntax errors. The following methods implement this incremental parser. =head2 incr_parse $json->incr_parse( [$string] ) # void context $obj_or_undef = $json->incr_parse( [$string] ) # scalar context @obj_or_empty = $json->incr_parse( [$string] ) # list context This is the central parsing function. It can both append new text and extract objects from the stream accumulated so far (both of these functions are optional). If C<$string> is given, then this string is appended to the already existing JSON fragment stored in the C<$json> object. After that, if the function is called in void context, it will simply return without doing anything further. This can be used to add more text in as many chunks as you want. If the method is called in scalar context, then it will try to extract exactly I JSON object. If that is successful, it will return this object, otherwise it will return C. If there is a parse error, this method will croak just as C would do (one can then use C to skip the erroneous part). This is the most common way of using the method. And finally, in list context, it will try to extract as many objects from the stream as it can find and return them, or the empty list otherwise. For this to work, there must be no separators (other than whitespace) between the JSON objects or arrays, instead they must be concatenated back-to-back. If an error occurs, an exception will be raised as in the scalar context case. Note that in this case, any previously-parsed JSON texts will be lost. Example: Parse some JSON arrays/objects in a given string and return them. my @objs = JSON::PP->new->incr_parse ("[5][7][1,2]"); =head2 incr_text $lvalue_string = $json->incr_text This method returns the currently stored JSON fragment as an lvalue, that is, you can manipulate it. This I works when a preceding call to C in I successfully returned an object. Under all other circumstances you must not call this function (I mean it. although in simple tests it might actually work, it I fail under real world conditions). As a special exception, you can also call this method before having parsed anything. That means you can only use this function to look at or manipulate text before or after complete JSON objects, not while the parser is in the middle of parsing a JSON object. This function is useful in two cases: a) finding the trailing text after a JSON object or b) parsing multiple JSON objects separated by non-JSON text (such as commas). =head2 incr_skip $json->incr_skip This will reset the state of the incremental parser and will remove the parsed text from the input buffer so far. This is useful after C died, in which case the input buffer and incremental parser state is left unchanged, to skip the text parsed so far and to reset the parse state. The difference to C is that only text until the parse error occurred is removed. =head2 incr_reset $json->incr_reset This completely resets the incremental parser, that is, after this call, it will be as if the parser had never parsed anything. This is useful if you want to repeatedly parse JSON objects and want to ignore any trailing data, which means you have to reset the parser after each successful decode. =head1 MAPPING Most of this section is also taken from JSON::XS. This section describes how JSON::PP maps Perl values to JSON values and vice versa. These mappings are designed to "do the right thing" in most circumstances automatically, preserving round-tripping characteristics (what you put in comes out as something equivalent). For the more enlightened: note that in the following descriptions, lowercase I refers to the Perl interpreter, while uppercase I refers to the abstract Perl language itself. =head2 JSON -> PERL =over 4 =item object A JSON object becomes a reference to a hash in Perl. No ordering of object keys is preserved (JSON does not preserve object key ordering itself). =item array A JSON array becomes a reference to an array in Perl. =item string A JSON string becomes a string scalar in Perl - Unicode codepoints in JSON are represented by the same codepoints in the Perl string, so no manual decoding is necessary. =item number A JSON number becomes either an integer, numeric (floating point) or string scalar in perl, depending on its range and any fractional parts. On the Perl level, there is no difference between those as Perl handles all the conversion details, but an integer may take slightly less memory and might represent more values exactly than floating point numbers. If the number consists of digits only, JSON::PP will try to represent it as an integer value. If that fails, it will try to represent it as a numeric (floating point) value if that is possible without loss of precision. Otherwise it will preserve the number as a string value (in which case you lose roundtripping ability, as the JSON number will be re-encoded to a JSON string). Numbers containing a fractional or exponential part will always be represented as numeric (floating point) values, possibly at a loss of precision (in which case you might lose perfect roundtripping ability, but the JSON number will still be re-encoded as a JSON number). Note that precision is not accuracy - binary floating point values cannot represent most decimal fractions exactly, and when converting from and to floating point, JSON::PP only guarantees precision up to but not including the least significant bit. When C is enabled, big integer values and any numeric values will be converted into L and L objects respectively, without becoming string scalars or losing precision. =item true, false These JSON atoms become C and C, respectively. They are overloaded to act almost exactly like the numbers C<1> and C<0>. You can check whether a scalar is a JSON boolean by using the C function. =item null A JSON null atom becomes C in Perl. =item shell-style comments (C<< # I >>) As a nonstandard extension to the JSON syntax that is enabled by the C setting, shell-style comments are allowed. They can start anywhere outside strings and go till the end of the line. =item tagged values (C<< (I)I >>). Another nonstandard extension to the JSON syntax, enabled with the C setting, are tagged values. In this implementation, the I must be a perl package/class name encoded as a JSON string, and the I must be a JSON array encoding optional constructor arguments. See L, below, for details. =back =head2 PERL -> JSON The mapping from Perl to JSON is slightly more difficult, as Perl is a truly typeless language, so we can only guess which JSON type is meant by a Perl value. =over 4 =item hash references Perl hash references become JSON objects. As there is no inherent ordering in hash keys (or JSON objects), they will usually be encoded in a pseudo-random order. JSON::PP can optionally sort the hash keys (determined by the I flag and/or I property), so the same data structure will serialise to the same JSON text (given same settings and version of JSON::PP), but this incurs a runtime overhead and is only rarely useful, e.g. when you want to compare some JSON text against another for equality. =item array references Perl array references become JSON arrays. =item other references Other unblessed references are generally not allowed and will cause an exception to be thrown, except for references to the integers C<0> and C<1>, which get turned into C and C atoms in JSON. You can also use C and C to improve readability. to_json [\0, JSON::PP::true] # yields [false,true] =item JSON::PP::true, JSON::PP::false These special values become JSON true and JSON false values, respectively. You can also use C<\1> and C<\0> directly if you want. =item JSON::PP::null This special value becomes JSON null. =item blessed objects Blessed objects are not directly representable in JSON, but C allows various ways of handling objects. See L, below, for details. =item simple scalars Simple Perl scalars (any scalar that is not a reference) are the most difficult objects to encode: JSON::PP will encode undefined scalars as JSON C values, scalars that have last been used in a string context before encoding as JSON strings, and anything else as number value: # dump as number encode_json [2] # yields [2] encode_json [-3.0e17] # yields [-3e+17] my $value = 5; encode_json [$value] # yields [5] # used as string, so dump as string print $value; encode_json [$value] # yields ["5"] # undef becomes null encode_json [undef] # yields [null] You can force the type to be a JSON string by stringifying it: my $x = 3.1; # some variable containing a number "$x"; # stringified $x .= ""; # another, more awkward way to stringify print $x; # perl does it for you, too, quite often # (but for older perls) You can force the type to be a JSON number by numifying it: my $x = "3"; # some variable containing a string $x += 0; # numify it, ensuring it will be dumped as a number $x *= 1; # same thing, the choice is yours. You can not currently force the type in other, less obscure, ways. Since version 2.91_01, JSON::PP uses a different number detection logic that converts a scalar that is possible to turn into a number safely. The new logic is slightly faster, and tends to help people who use older perl or who want to encode complicated data structure. However, this may results in a different JSON text from the one JSON::XS encodes (and thus may break tests that compare entire JSON texts). If you do need the previous behavior for compatibility or for finer control, set PERL_JSON_PP_USE_B environmental variable to true before you C JSON::PP (or JSON.pm). Note that numerical precision has the same meaning as under Perl (so binary to decimal conversion follows the same rules as in Perl, which can differ to other languages). Also, your perl interpreter might expose extensions to the floating point numbers of your platform, such as infinities or NaN's - these cannot be represented in JSON, and it is an error to pass those in. JSON::PP (and JSON::XS) trusts what you pass to C method (or C function) is a clean, validated data structure with values that can be represented as valid JSON values only, because it's not from an external data source (as opposed to JSON texts you pass to C or C, which JSON::PP considers tainted and doesn't trust). As JSON::PP doesn't know exactly what you and consumers of your JSON texts want the unexpected values to be (you may want to convert them into null, or to stringify them with or without normalisation (string representation of infinities/NaN may vary depending on platforms), or to croak without conversion), you're advised to do what you and your consumers need before you encode, and also not to numify values that may start with values that look like a number (including infinities/NaN), without validating. =back =head2 OBJECT SERIALISATION As JSON cannot directly represent Perl objects, you have to choose between a pure JSON representation (without the ability to deserialise the object automatically again), and a nonstandard extension to the JSON syntax, tagged values. =head3 SERIALISATION What happens when C encounters a Perl object depends on the C, C, C and C settings, which are used in this order: =over 4 =item 1. C is enabled and the object has a C method. In this case, C creates a tagged JSON value, using a nonstandard extension to the JSON syntax. This works by invoking the C method on the object, with the first argument being the object to serialise, and the second argument being the constant string C to distinguish it from other serialisers. The C method can return any number of values (i.e. zero or more). These values and the paclkage/classname of the object will then be encoded as a tagged JSON value in the following format: ("classname")[FREEZE return values...] e.g.: ("URI")["http://www.google.com/"] ("MyDate")[2013,10,29] ("ImageData::JPEG")["Z3...VlCg=="] For example, the hypothetical C C method might use the objects C and C members to encode the object: sub My::Object::FREEZE { my ($self, $serialiser) = @_; ($self->{type}, $self->{id}) } =item 2. C is enabled and the object has a C method. In this case, the C method of the object is invoked in scalar context. It must return a single scalar that can be directly encoded into JSON. This scalar replaces the object in the JSON text. For example, the following C method will convert all L objects to JSON strings when serialised. The fact that these values originally were L objects is lost. sub URI::TO_JSON { my ($uri) = @_; $uri->as_string } =item 3. C is enabled and the object is a C or C. The object will be serialised as a JSON number value. =item 4. C is enabled. The object will be serialised as a JSON null value. =item 5. none of the above If none of the settings are enabled or the respective methods are missing, C throws an exception. =back =head3 DESERIALISATION For deserialisation there are only two cases to consider: either nonstandard tagging was used, in which case C decides, or objects cannot be automatically be deserialised, in which case you can use postprocessing or the C or C callbacks to get some real objects our of your JSON. This section only considers the tagged value case: a tagged JSON object is encountered during decoding and C is disabled, a parse error will result (as if tagged values were not part of the grammar). If C is enabled, C will look up the C method of the package/classname used during serialisation (it will not attempt to load the package as a Perl module). If there is no such method, the decoding will fail with an error. Otherwise, the C method is invoked with the classname as first argument, the constant string C as second argument, and all the values from the JSON array (the values originally returned by the C method) as remaining arguments. The method must then return the object. While technically you can return any Perl scalar, you might have to enable the C setting to make that work in all cases, so better return an actual blessed reference. As an example, let's implement a C function that regenerates the C from the C example earlier: sub My::Object::THAW { my ($class, $serialiser, $type, $id) = @_; $class->new (type => $type, id => $id) } =head1 ENCODING/CODESET FLAG NOTES This section is taken from JSON::XS. The interested reader might have seen a number of flags that signify encodings or codesets - C, C and C. There seems to be some confusion on what these do, so here is a short comparison: C controls whether the JSON text created by C (and expected by C) is UTF-8 encoded or not, while C and C only control whether C escapes character values outside their respective codeset range. Neither of these flags conflict with each other, although some combinations make less sense than others. Care has been taken to make all flags symmetrical with respect to C and C, that is, texts encoded with any combination of these flag values will be correctly decoded when the same flags are used - in general, if you use different flag settings while encoding vs. when decoding you likely have a bug somewhere. Below comes a verbose discussion of these flags. Note that a "codeset" is simply an abstract set of character-codepoint pairs, while an encoding takes those codepoint numbers and I them, in our case into octets. Unicode is (among other things) a codeset, UTF-8 is an encoding, and ISO-8859-1 (= latin 1) and ASCII are both codesets I encodings at the same time, which can be confusing. =over 4 =item C flag disabled When C is disabled (the default), then C/C generate and expect Unicode strings, that is, characters with high ordinal Unicode values (> 255) will be encoded as such characters, and likewise such characters are decoded as-is, no changes to them will be done, except "(re-)interpreting" them as Unicode codepoints or Unicode characters, respectively (to Perl, these are the same thing in strings unless you do funny/weird/dumb stuff). This is useful when you want to do the encoding yourself (e.g. when you want to have UTF-16 encoded JSON texts) or when some other layer does the encoding for you (for example, when printing to a terminal using a filehandle that transparently encodes to UTF-8 you certainly do NOT want to UTF-8 encode your data first and have Perl encode it another time). =item C flag enabled If the C-flag is enabled, C/C will encode all characters using the corresponding UTF-8 multi-byte sequence, and will expect your input strings to be encoded as UTF-8, that is, no "character" of the input string must have any value > 255, as UTF-8 does not allow that. The C flag therefore switches between two modes: disabled means you will get a Unicode string in Perl, enabled means you get an UTF-8 encoded octet/binary string in Perl. =item C or C flags enabled With C (or C) enabled, C will escape characters with ordinal values > 255 (> 127 with C) and encode the remaining characters as specified by the C flag. If C is disabled, then the result is also correctly encoded in those character sets (as both are proper subsets of Unicode, meaning that a Unicode string with all character values < 256 is the same thing as a ISO-8859-1 string, and a Unicode string with all character values < 128 is the same thing as an ASCII string in Perl). If C is enabled, you still get a correct UTF-8-encoded string, regardless of these flags, just some more characters will be escaped using C<\uXXXX> then before. Note that ISO-8859-1-I strings are not compatible with UTF-8 encoding, while ASCII-encoded strings are. That is because the ISO-8859-1 encoding is NOT a subset of UTF-8 (despite the ISO-8859-1 I being a subset of Unicode), while ASCII is. Surprisingly, C will ignore these flags and so treat all input values as governed by the C flag. If it is disabled, this allows you to decode ISO-8859-1- and ASCII-encoded strings, as both strict subsets of Unicode. If it is enabled, you can correctly decode UTF-8 encoded strings. So neither C nor C are incompatible with the C flag - they only govern when the JSON output engine escapes a character or not. The main use for C is to relatively efficiently store binary data as JSON, at the expense of breaking compatibility with most JSON decoders. The main use for C is to force the output to not contain characters with values > 127, which means you can interpret the resulting string as UTF-8, ISO-8859-1, ASCII, KOI8-R or most about any character set and 8-bit-encoding, and still get the same data structure back. This is useful when your channel for JSON transfer is not 8-bit clean or the encoding might be mangled in between (e.g. in mail), and works because ASCII is a proper subset of most 8-bit and multibyte encodings in use in the world. =back =head1 BUGS Please report bugs on a specific behavior of this module to RT or GitHub issues (preferred): L L As for new features and requests to change common behaviors, please ask the author of JSON::XS (Marc Lehmann, Eschmorp[at]schmorp.deE) first, by email (important!), to keep compatibility among JSON.pm backends. Generally speaking, if you need something special for you, you are advised to create a new module, maybe based on L, which is smaller and written in a much cleaner way than this module. =head1 SEE ALSO The F command line utility for quick experiments. L, L, and L for faster alternatives. L and L for easy migration. L and L for older perl users. RFC4627 (L) RFC7159 (L) RFC8259 (L) =head1 AUTHOR Makamaka Hannyaharamitu, Emakamaka[at]cpan.orgE =head1 CURRENT MAINTAINER Kenichi Ishigaki, Eishigaki[at]cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 2007-2016 by Makamaka Hannyaharamitu Most of the documentation is taken from JSON::XS by Marc Lehmann This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut JSON_PP $fatpacked{"JSON/PP/Boolean.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'JSON_PP_BOOLEAN'; package JSON::PP::Boolean; use strict; require overload; local $^W; overload::import('overload', "0+" => sub { ${$_[0]} }, "++" => sub { $_[0] = ${$_[0]} + 1 }, "--" => sub { $_[0] = ${$_[0]} - 1 }, fallback => 1, ); $JSON::PP::Boolean::VERSION = '4.02'; 1; __END__ =head1 NAME JSON::PP::Boolean - dummy module providing JSON::PP::Boolean =head1 SYNOPSIS # do not "use" yourself =head1 DESCRIPTION This module exists only to provide overload resolution for Storable and similar modules. See L for more info about this class. =head1 AUTHOR This idea is from L written by Marc Lehmann =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut JSON_PP_BOOLEAN $fatpacked{"Menlo.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO'; package Menlo; our $VERSION = "1.9019"; 1; __END__ =encoding utf8 =head1 NAME Menlo - A CPAN client =head1 DESCRIPTION Menlo is a backend for I, developed with the goal to replace L internals with a set of modules that are more flexible, extensible and easier to use. =head1 COMPATIBILITY Menlo is developed within L git repository at C subdirectory at L Menlo::CLI::Compat started off as a copy of App::cpanminus::script, but will go under a big refactoring to extract all the bits out of it. Hopefully the end result will be just a shim and translation layer to interpret command line options. =head1 MOTIVATION cpanm has been a popular choice of CPAN package installer for many developers, because it is lightweight, fast, and requires no configuration in most environments. Meanwhile, the way cpanm has been implemented (one God class, and all modules are packaged in one script with fatpacker) makes it difficult to extend, or modify the behaviors at a runtime, unless you decide to fork the code or monkeypatch its hidden backend class. cpanm also has no scriptable API or hook points, which means if you want to write a tool that works with cpanm, you basically have to work around its behavior by writing a shell wrapper, or parsing the output of its standard out or a build log file. Menlo will keep the best aspects of cpanm, which is dependencies free, configuration free, lightweight and fast to install CPAN modules. At the same time, it's impelmented as a standard perl module, available on CPAN, and you can extend its behavior by either using its modular interfaces, or writing plugins to hook into its behaviors. =head1 FAQ =over 4 =item Dependencies free? I see many prerequisites in Menlo. Menlo is a set of libraries and uses non-core CPAN modules as its dependencies. App-cpanminus distribution embeds Menlo and all of its runtime dependencies into a fatpacked binary, so that you can install App-cpanminus or Menlo without having any CPAN client to begin with. =item Is Menlo a new name for cpanm? Right now it's just a library name, but I'm comfortable calling this a new package name for cpanm 2's backend. =back =head1 AUTHOR Tatsuhiko Miyagawa Emiyagawa@bulknews.netE =head1 COPYRIGHT 2010- Tatsuhiko Miyagawa =head1 LICENSE This software is licensed under the same terms as Perl. =head1 SEE ALSO L =cut MENLO $fatpacked{"Menlo/Builder/Static.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_BUILDER_STATIC'; package Menlo::Builder::Static; use strict; use warnings; use CPAN::Meta; use ExtUtils::Config 0.003; use ExtUtils::Helpers 0.020 qw/make_executable split_like_shell man1_pagename man3_pagename detildefy/; use ExtUtils::Install qw/pm_to_blib install/; use ExtUtils::InstallPaths 0.002; use File::Basename qw/dirname/; use File::Find (); use File::Path qw/mkpath/; use File::Spec::Functions qw/catfile catdir rel2abs abs2rel splitdir curdir/; use Getopt::Long 2.36 qw/GetOptionsFromArray/; sub new { my($class, %args) = @_; bless { meta => $args{meta}, }, $class; } sub meta { my $self = shift; $self->{meta}; } sub manify { my ($input_file, $output_file, $section, $opts) = @_; return if -e $output_file && -M $input_file <= -M $output_file; my $dirname = dirname($output_file); mkpath($dirname, $opts->{verbose}) if not -d $dirname; require Pod::Man; Pod::Man->new(section => $section)->parse_from_file($input_file, $output_file); print "Manifying $output_file\n" if $opts->{verbose} && $opts->{verbose} > 0; return; } sub find { my ($pattern, $dir) = @_; my @ret; File::Find::find(sub { push @ret, $File::Find::name if /$pattern/ && -f }, $dir) if -d $dir; return @ret; } my %actions = ( build => sub { my %opt = @_; my %modules = map { $_ => catfile('blib', $_) } find(qr/\.p(?:m|od)$/, 'lib'); my %scripts = map { $_ => catfile('blib', $_) } find(qr//, 'script'); my %shared = map { $_ => catfile(qw/blib lib auto share dist/, $opt{meta}->name, abs2rel($_, 'share')) } find(qr//, 'share'); pm_to_blib({ %modules, %scripts, %shared }, catdir(qw/blib lib auto/)); make_executable($_) for values %scripts; mkpath(catdir(qw/blib arch/), $opt{verbose}); if ($opt{install_paths}->install_destination('bindoc') && $opt{install_paths}->is_default_installable('bindoc')) { manify($_, catfile('blib', 'bindoc', man1_pagename($_)), $opt{config}->get('man1ext'), \%opt) for keys %scripts; } if ($opt{install_paths}->install_destination('libdoc') && $opt{install_paths}->is_default_installable('libdoc')) { manify($_, catfile('blib', 'libdoc', man3_pagename($_)), $opt{config}->get('man3ext'), \%opt) for keys %modules; } 1; }, test => sub { my %opt = @_; die "Must run `./Build build` first\n" if not -d 'blib'; require TAP::Harness::Env; my %test_args = ( (verbosity => $opt{verbose}) x!! exists $opt{verbose}, (jobs => $opt{jobs}) x!! exists $opt{jobs}, (color => 1) x !!-t STDOUT, lib => [ map { rel2abs(catdir(qw/blib/, $_)) } qw/arch lib/ ], ); my $tester = TAP::Harness::Env->create(\%test_args); $tester->runtests(sort +find(qr/\.t$/, 't'))->has_errors and return; 1; }, install => sub { my %opt = @_; die "Must run `./Build build` first\n" if not -d 'blib'; install($opt{install_paths}->install_map, @opt{qw/verbose dry_run uninst/}); 1; }, ); sub build { my $self = shift; my $action = @_ && $_[0] =~ /\A\w+\z/ ? shift @_ : 'build'; die "No such action '$action'\n" if not $actions{$action}; my %opt; GetOptionsFromArray([@$_], \%opt, qw/install_base=s install_path=s% installdirs=s destdir=s prefix=s config=s% uninst:1 verbose:1 dry_run:1 pureperl-only:1 create_packlist=i jobs=i/) for ($self->{env}, $self->{configure_args}, \@_); $_ = detildefy($_) for grep { defined } @opt{qw/install_base destdir prefix/}, values %{ $opt{install_path} }; @opt{ 'config', 'meta' } = (ExtUtils::Config->new($opt{config}), $self->meta); $actions{$action}->(%opt, install_paths => ExtUtils::InstallPaths->new(%opt, dist_name => $opt{meta}->name)); } sub configure { my $self = shift; $self->{env} = defined $ENV{PERL_MB_OPT} ? [split_like_shell($ENV{PERL_MB_OPT})] : []; $self->{configure_args} = [@_]; $self->meta->save(@$_) for ['MYMETA.json'], [ 'MYMETA.yml' => { version => 1.4 } ]; } 1; =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2011 by Leon Timmermans, David Golden. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut MENLO_BUILDER_STATIC $fatpacked{"Menlo/CLI/Compat.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_CLI_COMPAT'; package Menlo::CLI::Compat; use strict; use Config; use Cwd (); use Menlo; use Menlo::Dependency; use Menlo::Util qw(WIN32); use File::Basename (); use File::Find (); use File::Path (); use File::Spec (); use File::Copy (); use File::Temp (); use File::Which qw(which); use Getopt::Long (); use Symbol (); use version (); use constant BAD_TAR => ($^O eq 'solaris' || $^O eq 'hpux'); use constant CAN_SYMLINK => eval { symlink("", ""); 1 }; our $VERSION = '1.9022'; if ($INC{"App/FatPacker/Trace.pm"}) { require version::vpp; } sub qs($) { Menlo::Util::shell_quote($_[0]); } sub determine_home { my $class = shift; my $homedir = $ENV{HOME} || eval { require File::HomeDir; File::HomeDir->my_home } || join('', @ENV{qw(HOMEDRIVE HOMEPATH)}); # Win32 if (WIN32) { require Win32; # no fatpack $homedir = Win32::GetShortPathName($homedir); } return "$homedir/.cpanm"; } sub new { my $class = shift; my $self = bless { name => "Menlo", home => $class->determine_home, cmd => 'install', seen => {}, notest => undef, test_only => undef, installdeps => undef, force => undef, sudo => undef, make => undef, verbose => undef, quiet => undef, interactive => undef, log => undef, mirrors => [], mirror_only => undef, mirror_index => undef, cpanmetadb => "http://cpanmetadb.plackperl.org/v1.0/", perl => $^X, argv => [], local_lib => undef, self_contained => undef, exclude_vendor => undef, prompt_timeout => 0, prompt => undef, configure_timeout => 60, build_timeout => 3600, test_timeout => 1800, try_lwp => 1, try_wget => 1, try_curl => 1, uninstall_shadows => ($] < 5.012), skip_installed => 1, skip_satisfied => 0, static_install => 1, auto_cleanup => 7, # days pod2man => 1, installed_dists => 0, install_types => ['requires'], with_develop => 0, with_configure => 0, showdeps => 0, scandeps => 0, scandeps_tree => [], format => 'tree', save_dists => undef, skip_configure => 0, verify => 0, report_perl_version => !$class->maybe_ci, build_args => {}, features => {}, pure_perl => 0, cpanfile_path => 'cpanfile', @_, }, $class; $self; } sub env { my($self, $key) = @_; $ENV{"PERL_CPANM_" . $key}; } sub maybe_ci { my $class = shift; grep $ENV{$_}, qw( TRAVIS CI AUTOMATED_TESTING AUTHOR_TESTING ); } sub install_type_handlers { my $self = shift; my @handlers; for my $type (qw( recommends suggests )) { push @handlers, "with-$type" => sub { my %uniq; $self->{install_types} = [ grep !$uniq{$_}++, @{$self->{install_types}}, $type ]; }; push @handlers, "without-$type" => sub { $self->{install_types} = [ grep $_ ne $type, @{$self->{install_types}} ]; }; } @handlers; } sub build_args_handlers { my $self = shift; my @handlers; for my $phase (qw( configure build test install )) { push @handlers, "$phase-args=s" => \($self->{build_args}{$phase}); } @handlers; } sub parse_options { my $self = shift; local @ARGV = @{$self->{argv}}; push @ARGV, grep length, split /\s+/, $self->env('OPT'); push @ARGV, @_; Getopt::Long::Configure("bundling"); Getopt::Long::GetOptions( 'f|force' => sub { $self->{skip_installed} = 0; $self->{force} = 1 }, 'n|notest!' => \$self->{notest}, 'test-only' => sub { $self->{notest} = 0; $self->{skip_installed} = 0; $self->{test_only} = 1 }, 'S|sudo!' => \$self->{sudo}, 'v|verbose' => \$self->{verbose}, 'verify!' => \$self->{verify}, 'q|quiet!' => \$self->{quiet}, 'h|help' => sub { $self->{action} = 'show_help' }, 'V|version' => sub { $self->{action} = 'show_version' }, 'perl=s' => sub { $self->diag("--perl is deprecated since it's known to be fragile in figuring out dependencies. Run `$_[1] -S cpanm` instead.\n", 1); $self->{perl} = $_[1]; }, 'l|local-lib=s' => sub { $self->{local_lib} = $self->maybe_abs($_[1]) }, 'L|local-lib-contained=s' => sub { $self->{local_lib} = $self->maybe_abs($_[1]); $self->{self_contained} = 1; $self->{pod2man} = undef; }, 'self-contained!' => \$self->{self_contained}, 'exclude-vendor!' => \$self->{exclude_vendor}, 'mirror=s@' => $self->{mirrors}, 'mirror-only!' => \$self->{mirror_only}, 'mirror-index=s' => sub { $self->{mirror_index} = $self->maybe_abs($_[1]) }, 'M|from=s' => sub { $self->{mirrors} = [$_[1]]; $self->{mirror_only} = 1; }, 'cpanmetadb=s' => \$self->{cpanmetadb}, 'cascade-search!' => \$self->{cascade_search}, 'prompt!' => \$self->{prompt}, 'installdeps' => \$self->{installdeps}, 'skip-installed!' => \$self->{skip_installed}, 'skip-satisfied!' => \$self->{skip_satisfied}, 'reinstall' => sub { $self->{skip_installed} = 0 }, 'interactive!' => \$self->{interactive}, 'i|install' => sub { $self->{cmd} = 'install' }, 'info' => sub { $self->{cmd} = 'info' }, 'look' => sub { $self->{cmd} = 'look'; $self->{skip_installed} = 0 }, 'U|uninstall' => sub { $self->{cmd} = 'uninstall' }, 'self-upgrade' => sub { $self->{action} = 'self_upgrade' }, 'uninst-shadows!' => \$self->{uninstall_shadows}, 'lwp!' => \$self->{try_lwp}, 'wget!' => \$self->{try_wget}, 'curl!' => \$self->{try_curl}, 'auto-cleanup=s' => \$self->{auto_cleanup}, 'man-pages!' => \$self->{pod2man}, 'scandeps' => \$self->{scandeps}, 'showdeps' => sub { $self->{showdeps} = 1; $self->{skip_installed} = 0 }, 'format=s' => \$self->{format}, 'save-dists=s' => sub { $self->{save_dists} = $self->maybe_abs($_[1]); }, 'skip-configure!' => \$self->{skip_configure}, 'static-install!' => \$self->{static_install}, 'dev!' => \$self->{dev_release}, 'metacpan!' => \$self->{metacpan}, 'report-perl-version!' => \$self->{report_perl_version}, 'configure-timeout=i' => \$self->{configure_timeout}, 'build-timeout=i' => \$self->{build_timeout}, 'test-timeout=i' => \$self->{test_timeout}, 'with-develop' => \$self->{with_develop}, 'without-develop' => sub { $self->{with_develop} = 0 }, 'with-configure' => \$self->{with_configure}, 'without-configure' => sub { $self->{with_configure} = 0 }, 'with-feature=s' => sub { $self->{features}{$_[1]} = 1 }, 'without-feature=s' => sub { $self->{features}{$_[1]} = 0 }, 'with-all-features' => sub { $self->{features}{__all} = 1 }, 'pp|pureperl!' => \$self->{pure_perl}, "cpanfile=s" => \$self->{cpanfile_path}, $self->install_type_handlers, $self->build_args_handlers, ); if (!@ARGV && $0 ne '-' && !-t STDIN){ # e.g. # cpanm < author/requires.cpanm push @ARGV, $self->load_argv_from_fh(\*STDIN); $self->{load_from_stdin} = 1; } $self->{argv} = \@ARGV; } sub check_upgrade { my $self = shift; my $install_base = $ENV{PERL_LOCAL_LIB_ROOT} ? $self->local_lib_target($ENV{PERL_LOCAL_LIB_ROOT}) : $Config{installsitebin}; if ($0 eq '-') { # run from curl, that's fine return; } elsif ($0 !~ /^$install_base/) { if ($0 =~ m!perlbrew/bin!) { die <{_checked}++; $self->bootstrap_local_lib; } sub setup_verify { my $self = shift; my $has_modules = eval { require Module::Signature; require Digest::SHA; 1 }; $self->{cpansign} = which('cpansign'); unless ($has_modules && $self->{cpansign}) { warn "WARNING: Module::Signature and Digest::SHA is required for distribution verifications.\n"; $self->{verify} = 0; } } sub parse_module_args { my($self, $module) = @_; # Plack@1.2 -> Plack~"==1.2" # BUT don't expand @ in git URLs $module =~ s/^([A-Za-z0-9_:]+)@([v\d\._]+)$/$1~== $2/; # Plack~1.20, DBI~"> 1.0, <= 2.0" if ($module =~ /\~[v\d\._,\!<>= ]+$/) { return split '~', $module, 2; } else { return $module, undef; } } sub run { my $self = shift; my $code; eval { $code = ($self->_doit == 0); }; if (my $e = $@) { warn $e; $code = 1; } $self->{status} = $code; } sub status { $_[0]->{status}; } sub _doit { my $self = shift; $self->setup_home; $self->init_tools; $self->setup_verify if $self->{verify}; if (my $action = $self->{action}) { $self->$action() and return 1; } return $self->show_help(1) unless @{$self->{argv}} or $self->{load_from_stdin}; $self->configure_mirrors; my $cwd = Cwd::cwd; my @fail; for my $module (@{$self->{argv}}) { if ($module =~ s/\.pm$//i) { my ($volume, $dirs, $file) = File::Spec->splitpath($module); $module = join '::', grep { $_ } File::Spec->splitdir($dirs), $file; } ($module, my $version) = $self->parse_module_args($module); $self->chdir($cwd); if ($self->{cmd} eq 'uninstall') { $self->uninstall_module($module) or push @fail, $module; } else { $self->install_module($module, 0, $version) or push @fail, $module; } } if ($self->{base} && $self->{auto_cleanup}) { $self->cleanup_workdirs; } if ($self->{installed_dists}) { my $dists = $self->{installed_dists} > 1 ? "distributions" : "distribution"; $self->diag("$self->{installed_dists} $dists installed\n", 1); } if ($self->{scandeps}) { $self->dump_scandeps(); } # Workaround for older File::Temp's # where creating a tempdir with an implicit $PWD # causes tempdir non-cleanup if $PWD changes # as paths are stored internally without being resolved # absolutely. # https://rt.cpan.org/Public/Bug/Display.html?id=44924 $self->chdir($cwd); return !@fail; } sub setup_home { my $self = shift; $self->{home} = $self->env('HOME') if $self->env('HOME'); unless (_writable($self->{home})) { die "Can't write to cpanm home '$self->{home}': You should fix it with chown/chmod first.\n"; } $self->{base} = "$self->{home}/work/" . time . ".$$"; File::Path::mkpath([ $self->{base} ], 0, 0777); # native path because we use shell redirect $self->{log} = File::Spec->catfile($self->{base}, "build.log"); my $final_log = "$self->{home}/build.log"; { open my $out, ">$self->{log}" or die "$self->{log}: $!" } if (CAN_SYMLINK) { my $build_link = "$self->{home}/latest-build"; unlink $build_link; symlink $self->{base}, $build_link; unlink $final_log; symlink $self->{log}, $final_log; } else { my $log = $self->{log}; my $home = $self->{home}; $self->{at_exit} = sub { my $self = shift; my $temp_log = "$home/build.log." . time . ".$$"; File::Copy::copy($log, $temp_log) && unlink($final_log); rename($temp_log, $final_log); } } $self->chat("cpanm ($self->{name}) $Menlo::VERSION on perl $] built for $Config{archname}\n" . "Work directory is $self->{base}\n"); } sub search_mirror_index_local { my ($self, $local, $module, $version) = @_; require CPAN::Common::Index::LocalPackage; my $index = CPAN::Common::Index::LocalPackage->new({ source => $local }); $self->search_common($index, { package => $module }, $version); } sub search_mirror_index { my ($self, $mirror, $module, $version) = @_; require Menlo::Index::Mirror; my $index = Menlo::Index::Mirror->new({ mirror => $mirror, cache => $self->source_for($mirror), fetcher => sub { $self->mirror(@_) }, }); $self->search_common($index, { package => $module }, $version); } sub search_common { my($self, $index, $search_args, $want_version) = @_; $index->refresh_index; my $found = $index->search_packages($search_args); $found = $self->cpan_module_common($found) if $found; return $found unless $self->{cascade_search}; if ($found) { if ($self->satisfy_version($found->{module}, $found->{module_version}, $want_version)) { return $found; } else { $self->chat("Found $found->{module} $found->{module_version} which doesn't satisfy $want_version.\n"); } } return; } sub with_version_range { my($self, $version) = @_; defined($version) && $version =~ /(?:<|!=|==)/; } sub search_metacpan { my($self, $module, $version, $dev_release) = @_; require Menlo::Index::MetaCPAN; $self->chat("Searching $module ($version) on metacpan ...\n"); my $index = Menlo::Index::MetaCPAN->new({ include_dev => $self->{dev_release} }); my $pkg = $self->search_common($index, { package => $module, version_range => $version }, $version); return $pkg if $pkg; $self->diag_fail("Finding $module ($version) on metacpan failed."); return; } sub search_database { my($self, $module, $version) = @_; my $found; if ($self->{dev_release} or $self->{metacpan}) { $found = $self->search_metacpan($module, $version, $self->{dev_release}) and return $found; $found = $self->search_cpanmetadb($module, $version, $self->{dev_release}) and return $found; } else { $found = $self->search_cpanmetadb($module, $version) and return $found; $found = $self->search_metacpan($module, $version) and return $found; } } sub search_cpanmetadb { my($self, $module, $version, $dev_release) = @_; require Menlo::Index::MetaDB; $self->chat("Searching $module ($version) on cpanmetadb ...\n"); my $args = { package => $module }; if ($self->with_version_range($version)) { $args->{version_range} = $version; } my $index = Menlo::Index::MetaDB->new({ uri => $self->{cpanmetadb} }); my $pkg = $self->search_common($index, $args, $version); return $pkg if $pkg; $self->diag_fail("Finding $module on cpanmetadb failed."); return; } sub search_module { my($self, $module, $version) = @_; if ($self->{mirror_index}) { $self->mask_output( chat => "Searching $module on mirror index $self->{mirror_index} ...\n" ); my $pkg = $self->search_mirror_index_local($self->{mirror_index}, $module, $version); return $pkg if $pkg; unless ($self->{cascade_search}) { $self->mask_output( diag_fail => "Finding $module ($version) on mirror index $self->{mirror_index} failed." ); return; } } unless ($self->{mirror_only}) { my $found = $self->search_database($module, $version); return $found if $found; } MIRROR: for my $mirror (@{ $self->{mirrors} }) { $self->mask_output( chat => "Searching $module on mirror $mirror ...\n" ); my $pkg = $self->search_mirror_index($mirror, $module, $version); return $pkg if $pkg; $self->mask_output( diag_fail => "Finding $module ($version) on mirror $mirror failed." ); } return; } sub source_for { my($self, $mirror) = @_; $mirror =~ s/[^\w\.\-]+/%/g; my $dir = "$self->{home}/sources/$mirror"; File::Path::mkpath([ $dir ], 0, 0777); return $dir; } sub load_argv_from_fh { my($self, $fh) = @_; my @argv; while(defined(my $line = <$fh>)){ chomp $line; $line =~ s/#.+$//; # comment $line =~ s/^\s+//; # trim spaces $line =~ s/\s+$//; # trim spaces push @argv, split ' ', $line if $line; } return @argv; } sub show_version { my $self = shift; print "cpanm ($self->{name}) version $VERSION ($0)\n"; print "perl version $] ($^X)\n\n"; print " \%Config:\n"; for my $key (qw( archname installsitelib installsitebin installman1dir installman3dir sitearchexp sitelibexp vendorarch vendorlibexp archlibexp privlibexp )) { print " $key=$Config{$key}\n" if $Config{$key}; } print " \%ENV:\n"; for my $key (grep /^PERL/, sort keys %ENV) { print " $key=$ENV{$key}\n"; } print " \@INC:\n"; for my $inc (@INC) { print " $inc\n" unless ref($inc) eq 'CODE'; } return 1; } sub show_help { my $self = shift; if ($_[0]) { print <splitdir($dir); while (@dir) { $dir = File::Spec->catdir(@dir); if (-e $dir) { return -w _; } pop @dir; } return; } sub maybe_abs { my($self, $lib) = @_; if ($lib eq '_' or $lib =~ /^~/ or File::Spec->file_name_is_absolute($lib)) { return $lib; } else { return File::Spec->canonpath(File::Spec->catdir(Cwd::cwd(), $lib)); } } sub local_lib_target { my($self, $root) = @_; # local::lib 1.008025 changed the order of PERL_LOCAL_LIB_ROOT (grep { $_ ne '' } split /\Q$Config{path_sep}/, $root)[0]; } sub bootstrap_local_lib { my $self = shift; # If -l is specified, use that. if ($self->{local_lib}) { return $self->setup_local_lib($self->{local_lib}); } # PERL_LOCAL_LIB_ROOT is defined. Run as local::lib mode without overwriting ENV if ($ENV{PERL_LOCAL_LIB_ROOT} && $ENV{PERL_MM_OPT}) { return $self->setup_local_lib($self->local_lib_target($ENV{PERL_LOCAL_LIB_ROOT}), 1); } # root, locally-installed perl or --sudo: don't care about install_base return if $self->{sudo} or (_writable($Config{installsitelib}) and _writable($Config{installsitebin})); # local::lib is configured in the shell -- yay if ($ENV{PERL_MM_OPT} and ($ENV{MODULEBUILDRC} or $ENV{PERL_MB_OPT})) { return; } $self->setup_local_lib; $self->diag(<module => $_ } @$config_deps; # M::B 0.38 and EUMM 6.58 for MYMETA # EU::Install 1.46 for local::lib my $reqs = CPAN::Meta::Requirements->from_string_hash({ 'Module::Build' => '0.38', 'ExtUtils::MakeMaker' => '6.58', 'ExtUtils::Install' => '1.46', }); if ($deps{"ExtUtils::MakeMaker"}) { $deps{"ExtUtils::MakeMaker"}->merge_with($reqs); } elsif ($deps{"Module::Build"}) { $deps{"Module::Build"}->merge_with($reqs); $deps{"ExtUtils::Install"} ||= Menlo::Dependency->new("ExtUtils::Install", 0, 'configure'); $deps{"ExtUtils::Install"}->merge_with($reqs); } @$config_deps = values %deps; } sub _core_only_inc { my($self, $base) = @_; require local::lib; ( local::lib->resolve_path(local::lib->install_base_arch_path($base)), local::lib->resolve_path(local::lib->install_base_perl_path($base)), (!$self->{exclude_vendor} ? grep {$_} @Config{qw(vendorarch vendorlibexp)} : ()), @Config{qw(archlibexp privlibexp)}, ); } sub _setup_local_lib_env { my($self, $base) = @_; $self->diag(<setup_env_hash_for($base, 0); } sub setup_local_lib { my($self, $base, $no_env) = @_; $base = undef if $base eq '_'; require local::lib; { local $0 = 'cpanm'; # so curl/wget | perl works $base ||= "~/perl5"; $base = local::lib->resolve_path($base); if ($self->{self_contained}) { my @inc = $self->_core_only_inc($base); $self->{search_inc} = [ @inc ]; } else { $self->{search_inc} = [ local::lib->install_base_arch_path($base), local::lib->install_base_perl_path($base), @INC, ]; } $self->_setup_local_lib_env($base) unless $no_env; $self->{local_lib} = $base; } } sub prompt_bool { my($self, $mess, $def) = @_; my $val = $self->prompt($mess, $def); return lc $val eq 'y'; } sub prompt { my($self, $mess, $def) = @_; my $isa_tty = -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT)) ; my $dispdef = defined $def ? "[$def] " : " "; $def = defined $def ? $def : ""; if (!$self->{prompt} || (!$isa_tty && eof STDIN)) { return $def; } local $|=1; local $\; my $ans; eval { local $SIG{ALRM} = sub { undef $ans; die "alarm\n" }; print STDOUT "$mess $dispdef"; alarm $self->{prompt_timeout} if $self->{prompt_timeout}; $ans = ; alarm 0; }; if ( defined $ans ) { chomp $ans; } else { # user hit ctrl-D or alarm timeout print STDOUT "\n"; } return (!defined $ans || $ans eq '') ? $def : $ans; } sub diag_ok { my($self, $msg) = @_; chomp $msg; $msg ||= "OK"; if ($self->{in_progress}) { $self->_diag("$msg\n"); $self->{in_progress} = 0; } $self->log("-> $msg\n"); } sub diag_fail { my($self, $msg, $always) = @_; chomp $msg; if ($self->{in_progress}) { $self->_diag("FAIL\n"); $self->{in_progress} = 0; } if ($msg) { $self->_diag("! $msg\n", $always, 1); $self->log("-> FAIL $msg\n"); } } sub diag_progress { my($self, $msg) = @_; chomp $msg; $self->{in_progress} = 1; $self->_diag("$msg ... "); $self->log("$msg\n"); } sub _diag { my($self, $msg, $always, $error) = @_; my $fh = $error ? *STDERR : *STDOUT; print {$fh} $msg if $always or $self->{verbose} or !$self->{quiet}; } sub diag { my($self, $msg, $always) = @_; $self->_diag($msg, $always); $self->log($msg); } sub chat { my $self = shift; print STDERR @_ if $self->{verbose}; $self->log(@_); } sub mask_output { my $self = shift; my $method = shift; $self->$method( $self->mask_uri_passwords(@_) ); } sub log { my $self = shift; open my $out, ">>$self->{log}"; print $out @_; } sub run_command { my($self, $cmd) = @_; # TODO move to a more appropriate runner method if (ref $cmd eq 'CODE') { if ($self->{verbose}) { return $cmd->(); } else { require Capture::Tiny; open my $logfh, ">>", $self->{log}; my $ret; Capture::Tiny::capture(sub { $ret = $cmd->() }, stdout => $logfh, stderr => $logfh); return $ret; } } if (WIN32) { $cmd = Menlo::Util::shell_quote(@$cmd) if ref $cmd eq 'ARRAY'; unless ($self->{verbose}) { $cmd .= " >> " . Menlo::Util::shell_quote($self->{log}) . " 2>&1"; } !system $cmd; } else { my $pid = fork; if ($pid) { waitpid $pid, 0; return !$?; } else { $self->run_exec($cmd); } } } sub run_exec { my($self, $cmd) = @_; if (ref $cmd eq 'ARRAY') { unless ($self->{verbose}) { open my $logfh, ">>", $self->{log}; open STDERR, '>&', $logfh; open STDOUT, '>&', $logfh; close $logfh; } exec @$cmd; } else { unless ($self->{verbose}) { $cmd .= " >> " . Menlo::Util::shell_quote($self->{log}) . " 2>&1"; } exec $cmd; } } sub run_timeout { my($self, $cmd, $timeout) = @_; return $self->run_command($cmd) if ref($cmd) eq 'CODE' || WIN32 || $self->{verbose} || !$timeout; my $pid = fork; if ($pid) { eval { local $SIG{ALRM} = sub { die "alarm\n" }; alarm $timeout; waitpid $pid, 0; alarm 0; }; if ($@ && $@ eq "alarm\n") { $self->diag_fail("Timed out (> ${timeout}s). Use --verbose to retry."); local $SIG{TERM} = 'IGNORE'; kill TERM => 0; waitpid $pid, 0; return; } return !$?; } elsif ($pid == 0) { $self->run_exec($cmd); } else { $self->chat("! fork failed: falling back to system()\n"); $self->run_command($cmd); } } sub append_args { my($self, $cmd, $phase) = @_; return $cmd if ref $cmd ne 'ARRAY'; if (my $args = $self->{build_args}{$phase}) { $cmd = join ' ', Menlo::Util::shell_quote(@$cmd), $args; } $cmd; } sub _use_unsafe_inc { my($self, $dist) = @_; # if it's set in the env (i.e. user's shell), just use that if (exists $ENV{PERL_USE_UNSAFE_INC}) { return $ENV{PERL_USE_UNSAFE_INC}; } # it's set in CPAN Meta, prefer what the author says if (exists $dist->{meta}{x_use_unsafe_inc}) { $self->chat("Distribution opts in x_use_unsafe_inc: $dist->{meta}{x_use_unsafe_inc}\n"); return $dist->{meta}{x_use_unsafe_inc}; } # otherwise set to 1 as a default to allow for old modules return 1; } sub configure { my($self, $cmd, $dist, $depth) = @_; # trick AutoInstall local $ENV{PERL5_CPAN_IS_RUNNING} = local $ENV{PERL5_CPANPLUS_IS_RUNNING} = $$; # e.g. skip CPAN configuration on local::lib local $ENV{PERL5_CPANM_IS_RUNNING} = $$; my $use_default = !$self->{interactive}; local $ENV{PERL_MM_USE_DEFAULT} = $use_default; local $ENV{PERL_MM_OPT} = $ENV{PERL_MM_OPT}; local $ENV{PERL_MB_OPT} = $ENV{PERL_MB_OPT}; # skip man page generation unless ($self->{pod2man}) { $ENV{PERL_MM_OPT} .= " INSTALLMAN1DIR=none INSTALLMAN3DIR=none"; $ENV{PERL_MB_OPT} .= " --config installman1dir= --config installsiteman1dir= --config installman3dir= --config installsiteman3dir="; } # Lancaster Consensus if ($self->{pure_perl}) { $ENV{PERL_MM_OPT} .= " PUREPERL_ONLY=1"; $ENV{PERL_MB_OPT} .= " --pureperl-only"; } local $ENV{PERL_USE_UNSAFE_INC} = $self->_use_unsafe_inc($dist); $cmd = $self->append_args($cmd, 'configure') if $depth == 0; local $self->{verbose} = $self->{verbose} || $self->{interactive}; $self->run_timeout($cmd, $self->{configure_timeout}); } sub build { my($self, $cmd, $distname, $dist, $depth) = @_; local $ENV{PERL_MM_USE_DEFAULT} = !$self->{interactive}; local $ENV{PERL_USE_UNSAFE_INC} = $self->_use_unsafe_inc($dist); $cmd = $self->append_args($cmd, 'build') if $depth == 0; return 1 if $self->run_timeout($cmd, $self->{build_timeout}); while (1) { my $ans = lc $self->prompt("Building $distname failed.\nYou can s)kip, r)etry, e)xamine build log, or l)ook ?", "s"); return if $ans eq 's'; return $self->build($cmd, $distname, $dist, $depth) if $ans eq 'r'; $self->show_build_log if $ans eq 'e'; $self->look if $ans eq 'l'; } } sub test { my($self, $cmd, $distname, $dist, $depth) = @_; return 1 if $self->{notest}; # https://rt.cpan.org/Ticket/Display.html?id=48965#txn-1013385 local $ENV{PERL_MM_USE_DEFAULT} = !$self->{interactive}; # https://github.com/Perl-Toolchain-Gang/toolchain-site/blob/master/lancaster-consensus.md local $ENV{NONINTERACTIVE_TESTING} = !$self->{interactive}; local $ENV{PERL_USE_UNSAFE_INC} = $self->_use_unsafe_inc($dist); $cmd = $self->append_args($cmd, 'test') if $depth == 0; return 1 if $self->run_timeout($cmd, $self->{test_timeout}); if ($self->{force}) { $self->diag_fail("Testing $distname failed but installing it anyway."); return 1; } else { $self->diag_fail; while (1) { my $ans = lc $self->prompt("Testing $distname failed.\nYou can s)kip, r)etry, f)orce install, e)xamine build log, or l)ook ?", "s"); return if $ans eq 's'; return $self->test($cmd, $distname, $dist, $depth) if $ans eq 'r'; return 1 if $ans eq 'f'; $self->show_build_log if $ans eq 'e'; $self->look if $ans eq 'l'; } } } sub install { my($self, $cmd, $uninst_opts, $dist, $depth) = @_; if ($depth == 0 && $self->{test_only}) { return 1; } return $self->run_command($cmd) if ref $cmd eq 'CODE'; local $ENV{PERL_USE_UNSAFE_INC} = $self->_use_unsafe_inc($dist); if ($self->{sudo}) { unshift @$cmd, "sudo"; } if ($self->{uninstall_shadows} && !$ENV{PERL_MM_OPT}) { push @$cmd, @$uninst_opts; } $cmd = $self->append_args($cmd, 'install') if $depth == 0; $self->run_command($cmd); } sub look { my $self = shift; my $shell = $ENV{SHELL}; $shell ||= $ENV{COMSPEC} if WIN32; if ($shell) { my $cwd = Cwd::cwd; $self->diag("Entering $cwd with $shell\n"); system $shell; } else { $self->diag_fail("You don't seem to have a SHELL :/"); } } sub show_build_log { my $self = shift; my @pagers = ( $ENV{PAGER}, (WIN32 ? () : ('less')), 'more' ); my $pager; while (@pagers) { $pager = shift @pagers; next unless $pager; $pager = which($pager); next unless $pager; last; } if ($pager) { if (WIN32) { system "@{[ qs $pager ]} < @{[ qs $self->{log}]}"; } else { system $pager, $self->{log}; } } else { $self->diag_fail("You don't seem to have a PAGER :/"); } } sub chdir { my $self = shift; Cwd::chdir(File::Spec->canonpath($_[0])) or die "$_[0]: $!"; } sub configure_mirrors { my $self = shift; unless (@{$self->{mirrors}}) { $self->{mirrors} = [ 'http://www.cpan.org' ]; } for (@{$self->{mirrors}}) { s!^/!file:///!; s!/$!!; } } sub self_upgrade { my $self = shift; $self->check_upgrade; $self->{argv} = [ 'Menlo' ]; return; # continue } sub install_module { my($self, $module, $depth, $version, $dep) = @_; $self->check_libs; if ($self->{seen}{$module}++) { # TODO: circular dependencies $self->chat("Already tried $module. Skipping.\n"); return 1; } if ($self->{skip_satisfied}) { my($ok, $local) = $self->check_module($module, $version || 0); if ($ok) { $self->diag("You have $module ($local)\n", 1); return 1; } } my $dist = $self->resolve_name($module, $version, $dep); unless ($dist) { my $what = $module . ($version ? " ($version)" : ""); $self->diag_fail("Couldn't find module or a distribution $what", 1); return; } if ($dist->{distvname} && $self->{seen}{$dist->{distvname}}++) { $self->chat("Already tried $dist->{distvname}. Skipping.\n"); return 1; } if ($self->{cmd} eq 'info') { print $self->format_dist($dist), "\n"; return 1; } $dist->{depth} = $depth; # ugly hack if ($dist->{module}) { unless ($self->satisfy_version($dist->{module}, $dist->{module_version}, $version)) { $self->diag("Found $dist->{module} $dist->{module_version} which doesn't satisfy $version.\n", 1); return; } # If a version is requested, it has to be the exact same version, otherwise, check as if # it is the minimum version you need. my $cmp = $version ? "==" : ""; my $requirement = $dist->{module_version} ? "$cmp$dist->{module_version}" : 0; my($ok, $local) = $self->check_module($dist->{module}, $requirement); if ($self->{skip_installed} && $ok) { $self->diag("$dist->{module} is up to date. ($local)\n", 1); return 1; } } if ($dist->{dist} eq 'perl'){ $self->diag("skipping $dist->{pathname}\n"); return 1; } $self->diag("--> Working on $module\n"); $dist->{dir} ||= $self->fetch_module($dist); unless ($dist->{dir}) { $self->diag_fail("Failed to fetch distribution $dist->{distvname}", 1); return; } $self->chat("Entering $dist->{dir}\n"); $self->chdir($self->{base}); $self->chdir($dist->{dir}); if ($self->{cmd} eq 'look') { $self->look; return 1; } return $self->build_stuff($module, $dist, $depth); } sub uninstall_search_path { my $self = shift; $self->{local_lib} ? (local::lib->install_base_arch_path($self->{local_lib}), local::lib->install_base_perl_path($self->{local_lib})) : @Config{qw(installsitearch installsitelib)}; } sub uninstall_module { my ($self, $module) = @_; $self->check_libs; my @inc = $self->uninstall_search_path; my($metadata, $packlist) = $self->packlists_containing($module, \@inc); unless ($packlist) { $self->diag_fail(<uninstall_target($metadata, $packlist); $self->ask_permission($module, \@uninst_files) or return; $self->uninstall_files(@uninst_files, $packlist); $self->diag("Successfully uninstalled $module\n", 1); return 1; } sub packlists_containing { my($self, $module, $inc) = @_; require Module::Metadata; my $metadata = Module::Metadata->new_from_module($module, inc => $inc) or return; my $packlist; my $wanted = sub { return unless $_ eq '.packlist' && -f $_; for my $file ($self->unpack_packlist($File::Find::name)) { $packlist ||= $File::Find::name if $file eq $metadata->filename; } }; { require File::pushd; my $pushd = File::pushd::pushd(); my @search = grep -d $_, map File::Spec->catdir($_, 'auto'), @$inc; File::Find::find($wanted, @search); } return $metadata, $packlist; } sub uninstall_target { my($self, $metadata, $packlist) = @_; # If the module has a shadow install, or uses local::lib, then you can't just remove # all files in .packlist since it might have shadows in there if ($self->has_shadow_install($metadata) or $self->{local_lib}) { grep $self->should_unlink($_), $self->unpack_packlist($packlist); } else { $self->unpack_packlist($packlist); } } sub has_shadow_install { my($self, $metadata) = @_; # check if you have the module in site_perl *and* perl my @shadow = grep defined, map Module::Metadata->new_from_module($metadata->name, inc => [$_]), @INC; @shadow >= 2; } sub should_unlink { my($self, $file) = @_; # If local::lib is used, everything under the directory can be safely removed # Otherwise, bin and man files might be shared with the shadows i.e. site_perl vs perl # This is not 100% safe to keep the script there hoping to work with older version of .pm # files in the shadow, but there's nothing you can do about it. if ($self->{local_lib}) { $file =~ /^\Q$self->{local_lib}\E/; } else { !(grep $file =~ /^\Q$_\E/, @Config{qw(installbin installscript installman1dir installman3dir)}); } } sub ask_permission { my ($self, $module, $files) = @_; $self->diag("$module contains the following files:\n\n"); for my $file (@$files) { $self->diag(" $file\n"); } $self->diag("\n"); return 'force uninstall' if $self->{force}; local $self->{prompt} = 1; return $self->prompt_bool("Are you sure you want to uninstall $module?", 'y'); } sub unpack_packlist { my ($self, $packlist) = @_; open my $fh, '<', $packlist or die "$packlist: $!"; map { chomp; $_ } <$fh>; } sub uninstall_files { my ($self, @files) = @_; $self->diag("\n"); for my $file (@files) { $self->diag("Unlink: $file\n"); unlink $file or $self->diag_fail("$!: $file"); } $self->diag("\n"); return 1; } sub format_dist { my($self, $dist) = @_; # TODO support --dist-format? return "$dist->{cpanid}/$dist->{filename}"; } sub trim { local $_ = shift; tr/\n/ /d; s/^\s*|\s*$//g; $_; } sub fetch_module { my($self, $dist) = @_; $self->chdir($self->{base}); for my $uri (@{$dist->{uris}}) { $self->mask_output( diag_progress => "Fetching $uri" ); # Ugh, $dist->{filename} can contain sub directory my $filename = $dist->{filename} || $uri; my $name = File::Basename::basename($filename); my $cancelled; my $fetch = sub { my $file; eval { local $SIG{INT} = sub { $cancelled = 1; die "SIGINT\n" }; $self->mirror($uri, $name); $file = $name if -e $name; }; $self->diag("ERROR: " . trim("$@") . "\n", 1) if $@ && $@ ne "SIGINT\n"; return $file; }; my($try, $file); while ($try++ < 3) { $file = $fetch->(); last if $cancelled or $file; $self->mask_output( diag_fail => "Download $uri failed. Retrying ... "); } if ($cancelled) { $self->diag_fail("Download cancelled."); return; } unless ($file) { $self->mask_output( diag_fail => "Failed to download $uri"); next; } $self->diag_ok; $dist->{local_path} = File::Spec->rel2abs($name); my $dir = $self->unpack($file, $uri, $dist); next unless $dir; # unpack failed if (my $save = $self->{save_dists}) { # Only distros retrieved from CPAN have a pathname set my $path = $dist->{pathname} ? "$save/authors/id/$dist->{pathname}" : "$save/vendor/$file"; $self->chat("Copying $name to $path\n"); File::Path::mkpath([ File::Basename::dirname($path) ], 0, 0777); File::Copy::copy($file, $path) or warn $!; } return $dist, $dir; } } sub unpack { my($self, $file, $uri, $dist) = @_; if ($self->{verify}) { $self->verify_archive($file, $uri, $dist) or return; } $self->chat("Unpacking $file\n"); my $dir = $file =~ /\.zip/i ? $self->unzip($file) : $self->untar($file); unless ($dir) { $self->diag_fail("Failed to unpack $file: no directory"); } return $dir; } sub verify_checksums_signature { my($self, $chk_file) = @_; require Module::Signature; # no fatpack $self->chat("Verifying the signature of CHECKSUMS\n"); my $rv = eval { local $SIG{__WARN__} = sub {}; # suppress warnings my $v = Module::Signature::_verify($chk_file); $v == Module::Signature::SIGNATURE_OK(); }; if ($rv) { $self->chat("Verified OK!\n"); } else { $self->diag_fail("Verifying CHECKSUMS signature failed: $rv\n"); return; } return 1; } sub verify_archive { my($self, $file, $uri, $dist) = @_; unless ($dist->{cpanid}) { $self->chat("Archive '$file' does not seem to be from PAUSE. Skip verification.\n"); return 1; } (my $mirror = $uri) =~ s!/authors/id.*$!!; (my $chksum_uri = $uri) =~ s!/[^/]*$!/CHECKSUMS!; my $chk_file = $self->source_for($mirror) . "/$dist->{cpanid}.CHECKSUMS"; $self->mask_output( diag_progress => "Fetching $chksum_uri" ); $self->mirror($chksum_uri, $chk_file); unless (-e $chk_file) { $self->diag_fail("Fetching $chksum_uri failed.\n"); return; } $self->diag_ok; $self->verify_checksums_signature($chk_file) or return; $self->verify_checksum($file, $chk_file); } sub verify_checksum { my($self, $file, $chk_file) = @_; $self->chat("Verifying the SHA1 for $file\n"); open my $fh, "<$chk_file" or die "$chk_file: $!"; my $data = join '', <$fh>; $data =~ s/\015?\012/\n/g; require Safe; # no fatpack my $chksum = Safe->new->reval($data); if (!ref $chksum or ref $chksum ne 'HASH') { $self->diag_fail("! Checksum file downloaded from $chk_file is broken.\n"); return; } if (my $sha = $chksum->{$file}{sha256}) { my $hex = $self->sha_for(256, $file); if ($hex eq $sha) { $self->chat("Checksum for $file: Verified!\n"); } else { $self->diag_fail("Checksum mismatch for $file\n"); return; } } else { $self->chat("Checksum for $file not found in CHECKSUMS.\n"); return; } } sub sha_for { my($self, $alg, $file) = @_; require Digest::SHA; # no fatpack open my $fh, "<", $file or die "$file: $!"; my $dg = Digest::SHA->new($alg); my($data); while (read($fh, $data, 4096)) { $dg->add($data); } return $dg->hexdigest; } sub verify_signature { my($self, $dist) = @_; $self->diag_progress("Verifying the SIGNATURE file"); my $out = `@{[ qs $self->{cpansign} ]} -v --skip 2>&1`; $self->log($out); if ($out =~ /Signature verified OK/) { $self->diag_ok("Verified OK"); return 1; } else { $self->diag_fail("SIGNATURE verification for $dist->{filename} failed\n"); return; } } sub resolve_name { my($self, $module, $version, $dep) = @_; if ($dep && $dep->url) { if ($dep->url =~ m!authors/id/(.*)!) { return $self->cpan_dist($1, $dep->url); } else { return { uris => [ $dep->url ] }; } } if ($dep && $dep->dist) { return $self->cpan_dist($dep->dist, undef, $dep->mirror); } # Git if ($module =~ /(?:^git:|\.git(?:@.+)?$)/) { return $self->git_uri($module); } # URL if ($module =~ /^(ftp|https?|file):/) { if ($module =~ m!authors/id/(.*)!) { return $self->cpan_dist($1, $module); } else { return { uris => [ $module ] }; } } # Directory if ($module =~ m!^[\./]! && -d $module) { return { source => 'local', dir => Cwd::abs_path($module), }; } # File if (-f $module) { return { source => 'local', uris => [ "file://" . Cwd::abs_path($module) ], }; } # cpan URI if ($module =~ s!^cpan:///distfile/!!) { return $self->cpan_dist($module); } # PAUSEID/foo # P/PA/PAUSEID/foo if ($module =~ m!^(?:[A-Z]/[A-Z]{2}/)?([A-Z]{2}[\-A-Z0-9]*/.*)$!) { return $self->cpan_dist($1); } # Module name return $self->search_module($module, $version); } sub cpan_module_common { my($self, $match) = @_; (my $distfile = $match->{uri}) =~ s!^cpan:///distfile/!!; my $mirrors = $self->{mirrors}; if ($match->{download_uri}) { (my $mirror = $match->{download_uri}) =~ s!/authors/id/.*$!!; $mirrors = [$mirror]; } local $self->{mirrors} = $mirrors; return $self->cpan_module($match->{package}, $distfile, $match->{version}); } sub cpan_module { my($self, $module, $dist_file, $version) = @_; my $dist = $self->cpan_dist($dist_file); $dist->{module} = $module; $dist->{module_version} = $version if $version && $version ne 'undef'; return $dist; } sub cpan_dist { my($self, $dist, $url, $mirror) = @_; # strip trailing slash $mirror =~ s!/$!! if $mirror; $dist =~ s!^([A-Z]{2})!substr($1,0,1)."/".substr($1,0,2)."/".$1!e; require CPAN::DistnameInfo; my $d = CPAN::DistnameInfo->new($dist); if ($url) { $url = [ $url ] unless ref $url eq 'ARRAY'; } else { my $id = $d->cpanid; my $fn = substr($id, 0, 1) . "/" . substr($id, 0, 2) . "/" . $id . "/" . $d->filename; my @mirrors = $mirror ? ($mirror) : @{$self->{mirrors}}; my @urls = map "$_/authors/id/$fn", @mirrors; $url = \@urls, } return { $d->properties, source => 'cpan', uris => $url, }; } sub git_uri { my ($self, $uri) = @_; # similar to http://www.pip-installer.org/en/latest/logic.html#vcs-support # git URL has to end with .git when you need to use pin @ commit/tag/branch ($uri, my $commitish) = split /(?<=\.git)@/i, $uri, 2; my $dir = File::Temp::tempdir(CLEANUP => 1); $self->mask_output( diag_progress => "Cloning $uri" ); $self->run_command([ 'git', 'clone', $uri, $dir ]); unless (-e "$dir/.git") { $self->diag_fail("Failed cloning git repository $uri", 1); return; } if ($commitish) { require File::pushd; my $dir = File::pushd::pushd($dir); unless ($self->run_command([ 'git', 'checkout', $commitish ])) { $self->diag_fail("Failed to checkout '$commitish' in git repository $uri\n"); return; } } $self->diag_ok; return { source => 'local', dir => $dir, }; } sub core_version_for { my($self, $module) = @_; require Module::CoreList; # no fatpack unless (exists $Module::CoreList::version{$]+0}) { die sprintf("Module::CoreList %s (loaded from %s) doesn't seem to have entries for perl $]. " . "You're strongly recommended to upgrade Module::CoreList from CPAN.\n", $Module::CoreList::VERSION, $INC{"Module/CoreList.pm"}); } unless (exists $Module::CoreList::version{$]+0}{$module}) { return -1; } return $Module::CoreList::version{$]+0}{$module}; } sub search_inc { my $self = shift; $self->{search_inc} ||= do { # strip lib/ and fatlib/ from search path when booted from dev if (defined $::Bin) { [grep !/^\Q$::Bin\E\/..\/(?:fat)?lib$/, @INC] } else { [@INC] } }; } sub check_module { my($self, $mod, $want_ver) = @_; require Module::Metadata; my $meta = Module::Metadata->new_from_module($mod, inc => $self->search_inc) or return 0, undef; my $version = $meta->version; # When -L is in use, the version loaded from 'perl' library path # might be newer than (or actually wasn't core at) the version # that is shipped with the current perl if ($self->{self_contained} && $self->loaded_from_perl_lib($meta)) { $version = $self->core_version_for($mod); return 0, undef if $version && $version == -1; } $self->{local_versions}{$mod} = $version; if ($self->is_deprecated($meta)){ return 0, $version; } elsif ($self->satisfy_version($mod, $version, $want_ver)) { return 1, ($version || 'undef'); } else { return 0, $version; } } sub satisfy_version { my($self, $mod, $version, $want_ver) = @_; $want_ver = '0' unless defined($want_ver) && length($want_ver); require CPAN::Meta::Requirements; my $requirements = CPAN::Meta::Requirements->new; $requirements->add_string_requirement($mod, $want_ver); $requirements->accepts_module($mod, $version); } sub unsatisfy_how { my($self, $ver, $want_ver) = @_; if ($want_ver =~ /^[v0-9\.\_]+$/) { return "$ver < $want_ver"; } else { return "$ver doesn't satisfy $want_ver"; } } sub is_deprecated { my($self, $meta) = @_; my $deprecated = eval { require Module::CoreList; # no fatpack Module::CoreList::is_deprecated($meta->{module}); }; return $deprecated && $self->loaded_from_perl_lib($meta); } sub loaded_from_perl_lib { my($self, $meta) = @_; require Config; my @dirs = qw(archlibexp privlibexp); if ($self->{self_contained} && ! $self->{exclude_vendor} && $Config{vendorarch}) { unshift @dirs, qw(vendorarch vendorlibexp); } for my $dir (@dirs) { my $confdir = $Config{$dir}; if ($confdir eq substr($meta->filename, 0, length($confdir))) { return 1; } } return; } sub should_install { my($self, $mod, $ver) = @_; $self->chat("Checking if you have $mod $ver ... "); my($ok, $local) = $self->check_module($mod, $ver); if ($ok) { $self->chat("Yes ($local)\n") } elsif ($local) { $self->chat("No (" . $self->unsatisfy_how($local, $ver) . ")\n") } else { $self->chat("No\n") } return $mod unless $ok; return; } sub check_perl_version { my($self, $version) = @_; require CPAN::Meta::Requirements; my $req = CPAN::Meta::Requirements->from_string_hash({ perl => $version }); $req->accepts_module(perl => $]); } sub install_deps { my($self, $dir, $depth, @deps) = @_; my(@install, %seen, @fail); for my $dep (@deps) { next if $seen{$dep->module}; if ($dep->module eq 'perl') { if ($dep->is_requirement && !$self->check_perl_version($dep->version)) { $self->diag("Needs perl @{[$dep->version]}, you have $]\n"); push @fail, 'perl'; } } elsif ($self->should_install($dep->module, $dep->version)) { push @install, $dep; $seen{$dep->module} = 1; } } if (@install) { $self->diag("==> Found dependencies: " . join(", ", map $_->module, @install) . "\n"); } for my $dep (@install) { $self->install_module($dep->module, $depth + 1, $dep->version, $dep); } $self->chdir($self->{base}); $self->chdir($dir) if $dir; if ($self->{scandeps}) { return 1; # Don't check if dependencies are installed, since with --scandeps they aren't } my @not_ok = $self->unsatisfied_deps(@deps); if (@not_ok) { return 0, \@not_ok; } else { return 1; } } sub unsatisfied_deps { my($self, @deps) = @_; require CPAN::Meta::Check; require CPAN::Meta::Requirements; my $reqs = CPAN::Meta::Requirements->new; for my $dep (grep $_->is_requirement, @deps) { $reqs->add_string_requirement($dep->module => $dep->requires_version || '0'); } my $ret = CPAN::Meta::Check::check_requirements($reqs, 'requires', $self->{search_inc}); grep defined, values %$ret; } sub install_deps_bailout { my($self, $target, $dir, $depth, @deps) = @_; my($ok, $fail) = $self->install_deps($dir, $depth, @deps); if (!$ok) { $self->diag_fail("Installing the dependencies failed: " . join(", ", @$fail), 1); unless ($self->prompt_bool("Do you want to continue building $target anyway?", "n")) { $self->diag_fail("Bailing out the installation for $target.", 1); return; } } return 1; } sub build_stuff { my($self, $stuff, $dist, $depth) = @_; if ($self->{verify} && -e 'SIGNATURE') { $self->verify_signature($dist) or return; } require CPAN::Meta; my($meta_file) = grep -f, qw(META.json META.yml); if ($meta_file) { $self->chat("Checking configure dependencies from $meta_file\n"); $dist->{cpanmeta} = eval { CPAN::Meta->load_file($meta_file) }; } elsif ($dist->{dist} && $dist->{version}) { $self->chat("META.yml/json not found. Creating skeleton for it.\n"); $dist->{cpanmeta} = CPAN::Meta->new({ name => $dist->{dist}, version => $dist->{version} }); } $dist->{meta} = $dist->{cpanmeta} ? $dist->{cpanmeta}->as_struct : {}; if ($self->opts_in_static_install($dist->{cpanmeta})) { $dist->{static_install} = 1; } my @config_deps; if ($dist->{cpanmeta}) { push @config_deps, Menlo::Dependency->from_prereqs( $dist->{cpanmeta}->effective_prereqs, ['configure'], $self->{install_types}, ); } if (-e 'Build.PL' && !@config_deps) { push @config_deps, Menlo::Dependency->from_versions( { 'Module::Build' => '0.38' }, 'configure', ); } $self->merge_with_cpanfile($dist, \@config_deps); $self->upgrade_toolchain(\@config_deps); my $target = $dist->{meta}{name} ? "$dist->{meta}{name}-$dist->{meta}{version}" : $dist->{dir}; unless ($self->skip_configure($dist, $depth)) { $self->install_deps_bailout($target, $dist->{dir}, $depth, @config_deps) or return; } $self->diag_progress("Configuring $target"); my $configure_state = $self->configure_this($dist, $depth); $self->diag_ok($configure_state->{configured_ok} ? "OK" : "N/A"); if ($dist->{cpanmeta} && $dist->{source} eq 'cpan') { $dist->{provides} = $dist->{cpanmeta}{provides} || $self->extract_packages($dist->{cpanmeta}, "."); } # install direct 'test' dependencies for --installdeps, even with --notest # TODO: remove build dependencies for static install my $deps_only = $self->deps_only($depth); $dist->{want_phases} = $self->{notest} && !$self->deps_only($depth) ? [qw( build runtime )] : [qw( build test runtime )]; push @{$dist->{want_phases}}, 'develop' if $self->{with_develop} && $depth == 0; push @{$dist->{want_phases}}, 'configure' if $self->{with_configure} && $depth == 0; my @deps = $self->find_prereqs($dist); my $module_name = $self->find_module_name($configure_state) || $dist->{meta}{name}; $module_name =~ s/-/::/g; if ($self->{showdeps}) { for my $dep (@config_deps, @deps) { print $dep->module, ($dep->version ? ("~".$dep->version) : ""), "\n"; } return 1; } my $distname = $dist->{meta}{name} ? "$dist->{meta}{name}-$dist->{meta}{version}" : $stuff; my $walkup; if ($self->{scandeps}) { $walkup = $self->scandeps_append_child($dist); } $self->install_deps_bailout($distname, $dist->{dir}, $depth, @deps) or return; if ($self->{scandeps}) { unless ($configure_state->{configured_ok}) { my $diag = <{log} for details. ! You might have to install the following modules first to get --scandeps working correctly. DIAG if (@config_deps) { my @tree = @{$self->{scandeps_tree}}; $diag .= "!\n" . join("", map "! * $_->[0]{module}\n", @tree[0..$#tree-1]) if @tree; } $self->diag("!\n$diag!\n", 1); } $walkup->(); return 1; } if ($self->{installdeps} && $depth == 0) { if ($configure_state->{configured_ok}) { $self->diag("<== Installed dependencies for $stuff. Finishing.\n"); return 1; } else { $self->diag("! Configuring $distname failed. See $self->{log} for details.\n", 1); return; } } my $installed; if ($configure_state->{static_install}) { $self->diag_progress("Building " . ($self->{notest} ? "" : "and testing ") . $distname); $self->build(sub { $configure_state->{static_install}->build }, $distname, $dist, $depth) && $self->test(sub { $configure_state->{static_install}->build("test") }, $distname, $dist, $depth) && $self->install(sub { $configure_state->{static_install}->build("install") }, [], $dist, $depth) && $installed++; } elsif ($configure_state->{use_module_build} && -e 'Build' && -f _) { $self->diag_progress("Building " . ($self->{notest} ? "" : "and testing ") . $distname); $self->build([ $self->{perl}, "./Build" ], $distname, $dist, $depth) && $self->test([ $self->{perl}, "./Build", "test" ], $distname, $dist, $depth) && $self->install([ $self->{perl}, "./Build", "install" ], [ "--uninst", 1 ], $dist, $depth) && $installed++; } elsif ($self->{make} && -e 'Makefile') { $self->diag_progress("Building " . ($self->{notest} ? "" : "and testing ") . $distname); $self->build([ $self->{make} ], $distname, $dist, $depth) && $self->test([ $self->{make}, "test" ], $distname, $dist, $depth) && $self->install([ $self->{make}, "install" ], [ "UNINST=1" ], $dist, $depth) && $installed++; } else { my $why; my $configure_failed = $configure_state->{configured} && !$configure_state->{configured_ok}; if ($configure_failed) { $why = "Configure failed for $distname." } elsif ($self->{make}) { $why = "The distribution doesn't have a proper Makefile.PL/Build.PL" } else { $why = "Can't configure the distribution. You probably need to have 'make'." } $self->diag_fail("$why See $self->{log} for details.", 1); return; } if ($installed && $self->{test_only}) { $self->diag_ok; $self->diag("Successfully tested $distname\n", 1); } elsif ($installed) { my $local = $self->{local_versions}{$dist->{module} || ''}; my $version = $dist->{module_version} || $dist->{meta}{version} || $dist->{version}; my $reinstall = $local && ($local eq $version); my $action = $local && !$reinstall ? $self->is_downgrade($version, $local) ? "downgraded" : "upgraded" : undef; my $how = $reinstall ? "reinstalled $distname" : $local ? "installed $distname ($action from $local)" : "installed $distname" ; my $msg = "Successfully $how"; $self->diag_ok; $self->diag("$msg\n", 1); $self->{installed_dists}++; $self->save_meta($stuff, $dist, $module_name, \@config_deps, \@deps); return 1; } else { my $what = $self->{test_only} ? "Testing" : "Installing"; $self->diag_fail("$what $stuff failed. See $self->{log} for details. Retry with --force to force install it.", 1); return; } } sub is_downgrade { my($self, $va, $vb) = @_; eval { version::->new($va) < $vb }; } sub opts_in_static_install { my($self, $meta) = @_; return if !$self->{static_install}; # --sudo requires running a separate shell to prevent persistent configuration # uninstall-shadows (default on < 5.12) is not supported in BuildPL spec, yet. return if $self->{sudo} or $self->{uninstall_shadows}; return $meta->{x_static_install} && $meta->{x_static_install} == 1; } sub skip_configure { my($self, $dist, $depth) = @_; return 1 if $self->{skip_configure}; return 1 if $dist->{static_install}; return 1 if $self->no_dynamic_config($dist->{meta}) && $self->deps_only($depth); return; } sub no_dynamic_config { my($self, $meta) = @_; exists $meta->{dynamic_config} && $meta->{dynamic_config} == 0; } sub deps_only { my($self, $depth) = @_; ($self->{installdeps} && $depth == 0) or $self->{showdeps} or $self->{scandeps}; } sub perl_requirements { my($self, @requires) = @_; my @perl; for my $requires (grep defined, @requires) { if (exists $requires->{perl}) { push @perl, Menlo::Dependency->new(perl => $requires->{perl}); } } return @perl; } sub configure_this { my($self, $dist, $depth) = @_; my $deps_only = $self->deps_only($depth); if (-e $self->{cpanfile_path} && $deps_only) { require Module::CPANfile; $dist->{cpanfile} = eval { Module::CPANfile->load($self->{cpanfile_path}) }; $self->diag_fail($@, 1) if $@; $self->{cpanfile_global} ||= $dist->{cpanfile}; return { configured => 1, configured_ok => !!$dist->{cpanfile}, use_module_build => 0, }; } if ($self->{skip_configure}) { my $eumm = -e 'Makefile'; my $mb = -e 'Build' && -f _; return { configured => 1, configured_ok => $eumm || $mb, use_module_build => $mb, }; } if ($deps_only && $self->no_dynamic_config($dist->{meta})) { return { configured => 1, configured_ok => exists $dist->{meta}{prereqs}, use_module_build => 0, }; } my $state = {}; my $try_static = sub { if ($dist->{static_install}) { $self->chat("Distribution opts in x_static_install: $dist->{meta}{x_static_install}\n"); $self->static_install_configure($state, $dist, $depth); } }; my $try_eumm = sub { if (-e 'Makefile.PL') { $self->chat("Running Makefile.PL\n"); # NOTE: according to Devel::CheckLib, most XS modules exit # with 0 even if header files are missing, to avoid receiving # tons of FAIL reports in such cases. So exit code can't be # trusted if it went well. if ($self->configure([ $self->{perl}, "Makefile.PL" ], $dist, $depth)) { $state->{configured_ok} = -e 'Makefile'; } $state->{configured}++; } }; my $try_mb = sub { if (-e 'Build.PL') { $self->chat("Running Build.PL\n"); if ($self->configure([ $self->{perl}, "Build.PL" ], $dist, $depth)) { $state->{configured_ok} = -e 'Build' && -f _; } $state->{use_module_build}++; $state->{configured}++; } }; for my $try ($try_static, $try_mb, $try_eumm) { $try->(); last if $state->{configured_ok}; } unless ($state->{configured_ok}) { while (1) { my $ans = lc $self->prompt("Configuring $dist->{dist} failed.\nYou can s)kip, r)etry, e)xamine build log, or l)ook ?", "s"); last if $ans eq 's'; return $self->configure_this($dist, $depth) if $ans eq 'r'; $self->show_build_log if $ans eq 'e'; $self->look if $ans eq 'l'; } } return $state; } sub static_install_configure { my($self, $state, $dist, $depth) = @_; my $args = $depth == 0 ? $self->{build_args}{configure} : []; require Menlo::Builder::Static; my $builder = Menlo::Builder::Static->new(meta => $dist->{cpanmeta}); $self->configure(sub { $builder->configure($args || []) }, $dist, $depth); $state->{configured_ok} = 1; $state->{static_install} = $builder; $state->{configured}++; } sub find_module_name { my($self, $state) = @_; return unless $state->{configured_ok}; if ($state->{use_module_build} && -e "_build/build_params") { my $params = do { open my $in, "_build/build_params"; eval(join "", <$in>) }; return eval { $params->[2]{module_name} } || undef; } elsif (-e "Makefile") { open my $mf, "Makefile"; while (<$mf>) { if (/^\#\s+NAME\s+=>\s+(.*)/) { return eval($1); } } } return; } sub list_files { my $self = shift; if (-e 'MANIFEST') { require ExtUtils::Manifest; my $manifest = eval { ExtUtils::Manifest::manifind() } || {}; return sort { lc $a cmp lc $b } keys %$manifest; } else { require File::Find; my @files; my $finder = sub { my $name = $File::Find::name; $name =~ s!\.[/\\]!!; push @files, $name; }; File::Find::find($finder, "."); return sort { lc $a cmp lc $b } @files; } } sub extract_packages { my($self, $meta, $dir) = @_; my $try = sub { my $file = shift; return 0 if $file =~ m!^(?:x?t|inc|local|perl5|fatlib|_build)/!; return 1 unless $meta->{no_index}; return 0 if grep { $file =~ m!^$_/! } @{$meta->{no_index}{directory} || []}; return 0 if grep { $file eq $_ } @{$meta->{no_index}{file} || []}; return 1; }; require Parse::PMFile; my @files = grep { /\.pm(?:\.PL)?$/ && $try->($_) } $self->list_files; my $provides = { }; for my $file (@files) { my $parser = Parse::PMFile->new($meta, { UNSAFE => 1, ALLOW_DEV_VERSION => 1 }); my $packages = $parser->parse($file); while (my($package, $meta) = each %$packages) { $provides->{$package} ||= { file => $meta->{infile}, ($meta->{version} eq 'undef') ? () : (version => $meta->{version}), }; } } return $provides; } sub save_meta { my($self, $module, $dist, $module_name, $config_deps, $build_deps) = @_; return unless $dist->{distvname} && $dist->{source} eq 'cpan'; my $base = ($ENV{PERL_MM_OPT} || '') =~ /INSTALL_BASE=/ ? ($self->install_base($ENV{PERL_MM_OPT}) . "/lib/perl5") : $Config{sitelibexp}; my $provides = $dist->{provides}; File::Path::mkpath("blib/meta", 0, 0777); my $local = { name => $module_name, target => $module, version => exists $provides->{$module_name} ? ($provides->{$module_name}{version} || $dist->{version}) : $dist->{version}, dist => $dist->{distvname}, pathname => $dist->{pathname}, provides => $provides, }; require JSON::PP; open my $fh, ">", "blib/meta/install.json" or die $!; print $fh JSON::PP::encode_json($local); File::Copy::copy("MYMETA.json", "blib/meta/MYMETA.json"); my @cmd = ( ($self->{sudo} ? 'sudo' : ()), $^X, '-MExtUtils::Install=install', '-e', qq[install({ 'blib/meta' => '$base/$Config{archname}/.meta/$dist->{distvname}' })], ); $self->run_command(\@cmd); } sub install_base { my($self, $mm_opt) = @_; $mm_opt =~ /INSTALL_BASE=(\S+)/ and return $1; die "Your PERL_MM_OPT doesn't contain INSTALL_BASE"; } sub configure_features { my($self, $dist, @features) = @_; map $_->identifier, grep { $self->effective_feature($dist, $_) } @features; } sub effective_feature { my($self, $dist, $feature) = @_; if ($dist->{depth} == 0) { my $value = $self->{features}{$feature->identifier}; return $value if defined $value; return 1 if $self->{features}{__all}; } if ($self->{interactive}) { require CPAN::Meta::Requirements; $self->diag("[@{[ $feature->description ]}]\n", 1); my $req = CPAN::Meta::Requirements->new; for my $phase (@{$dist->{want_phases}}) { for my $type (@{$self->{install_types}}) { $req->add_requirements($feature->prereqs->requirements_for($phase, $type)); } } my $reqs = $req->as_string_hash; my @missing; for my $module (keys %$reqs) { if ($self->should_install($module, $req->{$module})) { push @missing, $module; } } if (@missing) { my $howmany = @missing; $self->diag("==> Found missing dependencies: " . join(", ", @missing) . "\n", 1); local $self->{prompt} = 1; return $self->prompt_bool("Install the $howmany optional module(s)?", "y"); } } return; } sub find_prereqs { my($self, $dist) = @_; my @deps = $self->extract_meta_prereqs($dist); if ($dist->{module} =~ /^Bundle::/i) { push @deps, $self->bundle_deps($dist); } $self->merge_with_cpanfile($dist, \@deps); return @deps; } sub merge_with_cpanfile { my($self, $dist, $deps) = @_; if ($self->{cpanfile_requirements} && !$dist->{cpanfile}) { for my $dep (@$deps) { $dep->merge_with($self->{cpanfile_requirements}); } } if ($self->{cpanfile_global}) { for my $dep (@$deps) { my $opts = $self->{cpanfile_global}->options_for_module($dep->module) or next; $dep->dist($opts->{dist}) if $opts->{dist}; $dep->mirror($opts->{mirror}) if $opts->{mirror}; $dep->url($opts->{url}) if $opts->{url}; } } } sub extract_meta_prereqs { my($self, $dist) = @_; if ($dist->{cpanfile}) { my @features = $self->configure_features($dist, $dist->{cpanfile}->features); my $prereqs = $dist->{cpanfile}->prereqs_with(@features); # TODO: creating requirements is useful even without cpanfile to detect conflicting prereqs $self->{cpanfile_requirements} = $prereqs->merged_requirements($dist->{want_phases}, ['requires']); return Menlo::Dependency->from_prereqs($prereqs, $dist->{want_phases}, $self->{install_types}); } require CPAN::Meta; my @meta = qw(MYMETA.json MYMETA.yml); if ($self->no_dynamic_config($dist->{meta})) { push @meta, qw(META.json META.yml); } my @deps; my($meta_file) = grep -f, @meta; if ($meta_file) { $self->chat("Checking dependencies from $meta_file ...\n"); my $mymeta = eval { CPAN::Meta->load_file($meta_file, { lazy_validation => 1 }) }; if ($mymeta) { $dist->{meta}{name} = $mymeta->name; $dist->{meta}{version} = $mymeta->version; return $self->extract_prereqs($mymeta, $dist); } } $self->diag_fail("No MYMETA file is found after configure. Your toolchain is too old?"); return; } sub bundle_deps { my($self, $dist) = @_; my $match; if ($dist->{module}) { $match = sub { my $meta = Module::Metadata->new_from_file($_[0]); $meta && ($meta->name eq $dist->{module}); }; } else { $match = sub { 1 }; } my @files; File::Find::find({ wanted => sub { push @files, File::Spec->rel2abs($_) if /\.pm$/i && $match->($_); }, no_chdir => 1, }, '.'); my @deps; for my $file (@files) { open my $pod, "<", $file or next; my $in_contents; while (<$pod>) { if (/^=head\d\s+CONTENTS/) { $in_contents = 1; } elsif (/^=/) { $in_contents = 0; } elsif ($in_contents) { /^(\S+)\s*(\S+)?/ and push @deps, Menlo::Dependency->new($1, $self->maybe_version($2)); } } } return @deps; } sub maybe_version { my($self, $string) = @_; return $string && $string =~ /^\.?\d/ ? $string : undef; } sub extract_prereqs { my($self, $meta, $dist) = @_; my @features = $self->configure_features($dist, $meta->features); my $prereqs = $meta->effective_prereqs(\@features)->clone; $self->adjust_prereqs($dist, $prereqs); return Menlo::Dependency->from_prereqs($prereqs, $dist->{want_phases}, $self->{install_types}); } sub adjust_prereqs { my($self, $dist, $prereqs) = @_; # Workaround for Module::Install 1.04 creating a bogus (higher) MakeMaker requirement that it needs in build_requires # Assuming MakeMaker requirement is already satisfied in configure_requires, there's no need to have higher version of # MakeMaker in build/test anyway. https://github.com/miyagawa/cpanminus/issues/463 if (-e "inc/Module/Install.pm") { for my $phase (qw( build test runtime )) { my $reqs = $prereqs->requirements_for($phase, 'requires'); if ($reqs->requirements_for_module('ExtUtils::MakeMaker')) { $reqs->clear_requirement('ExtUtils::MakeMaker'); $reqs->add_minimum('ExtUtils::MakeMaker' => 0); } } } # Static installation is optional and we're adding runtime dependencies if ($dist->{static_install}) { my $reqs = $prereqs->requirements_for('test' => 'requires'); $reqs->add_minimum('TAP::Harness::Env' => 0); } } sub cleanup_workdirs { my $self = shift; my $expire = time - 24 * 60 * 60 * $self->{auto_cleanup}; my @targets; opendir my $dh, "$self->{home}/work"; while (my $e = readdir $dh) { next if $e !~ /^(\d+)\.\d+$/; # {UNIX time}.{PID} my $time = $1; if ($time < $expire) { push @targets, "$self->{home}/work/$e"; } } if (@targets) { if (@targets >= 64) { $self->diag("Expiring " . scalar(@targets) . " work directories. This might take a while...\n"); } else { $self->chat("Expiring " . scalar(@targets) . " work directories.\n"); } File::Path::rmtree(\@targets, 0, 0); # safe = 0, since blib usually doesn't have write bits } } sub scandeps_append_child { my($self, $dist) = @_; my $new_node = [ $dist, [] ]; my $curr_node = $self->{scandeps_current} || [ undef, $self->{scandeps_tree} ]; push @{$curr_node->[1]}, $new_node; $self->{scandeps_current} = $new_node; return sub { $self->{scandeps_current} = $curr_node }; } sub dump_scandeps { my $self = shift; if ($self->{format} eq 'tree') { $self->walk_down(sub { my($dist, $depth) = @_; if ($depth == 0) { print "$dist->{distvname}\n"; } else { print " " x ($depth - 1); print "\\_ $dist->{distvname}\n"; } }, 1); } elsif ($self->{format} =~ /^dists?$/) { $self->walk_down(sub { my($dist, $depth) = @_; print $self->format_dist($dist), "\n"; }, 0); } elsif ($self->{format} eq 'json') { require JSON::PP; print JSON::PP::encode_json($self->{scandeps_tree}); } elsif ($self->{format} eq 'yaml') { require CPAN::Meta::YAML; print CPAN::Meta::YAML::Dump($self->{scandeps_tree}); } else { $self->diag("Unknown format: $self->{format}\n"); } } sub walk_down { my($self, $cb, $pre) = @_; $self->_do_walk_down($self->{scandeps_tree}, $cb, 0, $pre); } sub _do_walk_down { my($self, $children, $cb, $depth, $pre) = @_; # DFS - $pre determines when we call the callback for my $node (@$children) { $cb->($node->[0], $depth) if $pre; $self->_do_walk_down($node->[1], $cb, $depth + 1, $pre); $cb->($node->[0], $depth) unless $pre; } } sub DESTROY { my $self = shift; $self->{at_exit}->($self) if $self->{at_exit}; } # Utils sub mirror { my($self, $uri, $local) = @_; if ($uri =~ /^file:/) { $self->file_mirror($uri, $local); } else { $self->{http}->mirror($uri, $local); } } sub untar { $_[0]->{_backends}{untar}->(@_) }; sub unzip { $_[0]->{_backends}{unzip}->(@_) }; sub uri_to_file { my($self, $uri) = @_; # file:///path/to/file -> /path/to/file # file://C:/path -> C:/path if ($uri =~ s!file:/+!!) { $uri = "/$uri" unless $uri =~ m![a-zA-Z]:!; } return $uri; } sub file_get { my($self, $uri) = @_; my $file = $self->uri_to_file($uri); open my $fh, "<$file" or return; join '', <$fh>; } sub file_mirror { my($self, $uri, $path) = @_; my $file = $self->uri_to_file($uri); my $source_mtime = (stat $file)[9]; # Don't mirror a file that's already there (like the index) return 1 if -e $path && (stat $path)[9] >= $source_mtime; File::Copy::copy($file, $path); utime $source_mtime, $source_mtime, $path; } sub configure_http { my $self = shift; require HTTP::Tinyish; my @try = qw(HTTPTiny); unshift @try, 'Wget' if $self->{try_wget}; unshift @try, 'Curl' if $self->{try_curl}; unshift @try, 'LWP' if $self->{try_lwp}; my @protocol = ('http'); push @protocol, 'https' if grep /^https:/, @{$self->{mirrors}}; my $backend; for my $try (map "HTTP::Tinyish::$_", @try) { if (my $meta = HTTP::Tinyish->configure_backend($try)) { if ((grep $try->supports($_), @protocol) == @protocol) { for my $tool (sort keys %$meta){ (my $desc = $meta->{$tool}) =~ s/^(.*?)\n.*/$1/s; $self->chat("You have $tool: $desc\n"); } $backend = $try; last; } } } $backend->new(agent => "Menlo/$Menlo::VERSION", verify_SSL => 1); } sub init_tools { my $self = shift; return if $self->{initialized}++; if ($self->{make} = which($Config{make})) { $self->chat("You have make $self->{make}\n"); } $self->{http} = $self->configure_http; my $tar = which('tar'); my $tar_ver; my $maybe_bad_tar = sub { WIN32 || BAD_TAR || (($tar_ver = `@{[ qs $tar ]} --version 2>/dev/null`) =~ /GNU.*1\.13/i) }; if ($tar && !$maybe_bad_tar->()) { chomp $tar_ver; $self->chat("You have $tar: $tar_ver\n"); $self->{_backends}{untar} = sub { my($self, $tarfile) = @_; my $xf = ($self->{verbose} ? 'v' : '')."xf"; my $ar = $tarfile =~ /bz2$/ ? 'j' : 'z'; my($root, @others) = `@{[ qs $tar ]} ${ar}tf @{[ qs $tarfile ]}` or return undef; FILE: { chomp $root; $root =~ s!^\./!!; $root =~ s{^(.+?)/.*$}{$1}; if (!length($root)) { # archive had ./ as the first entry, so try again $root = shift(@others); redo FILE if $root; } } $self->run_command([ $tar, $ar.$xf, $tarfile ]); return $root if -d $root; $self->diag_fail("Bad archive: $tarfile"); return undef; } } elsif ( $tar and my $gzip = which('gzip') and my $bzip2 = which('bzip2')) { $self->chat("You have $tar, $gzip and $bzip2\n"); $self->{_backends}{untar} = sub { my($self, $tarfile) = @_; my $x = "x" . ($self->{verbose} ? 'v' : '') . "f -"; my $ar = $tarfile =~ /bz2$/ ? $bzip2 : $gzip; my($root, @others) = `@{[ qs $ar ]} -dc @{[ qs $tarfile ]} | @{[ qs $tar ]} tf -` or return undef; FILE: { chomp $root; $root =~ s!^\./!!; $root =~ s{^(.+?)/.*$}{$1}; if (!length($root)) { # archive had ./ as the first entry, so try again $root = shift(@others); redo FILE if $root; } } system "@{[ qs $ar ]} -dc @{[ qs $tarfile ]} | @{[ qs $tar ]} $x"; return $root if -d $root; $self->diag_fail("Bad archive: $tarfile"); return undef; } } elsif (eval { require Archive::Tar }) { # uses too much memory! $self->chat("Falling back to Archive::Tar $Archive::Tar::VERSION\n"); $self->{_backends}{untar} = sub { my $self = shift; my $t = Archive::Tar->new($_[0]); my($root, @others) = $t->list_files; FILE: { $root =~ s!^\./!!; $root =~ s{^(.+?)/.*$}{$1}; if (!length($root)) { # archive had ./ as the first entry, so try again $root = shift(@others); redo FILE if $root; } } $t->extract; return -d $root ? $root : undef; }; } else { $self->{_backends}{untar} = sub { die "Failed to extract $_[1] - You need to have tar or Archive::Tar installed.\n"; }; } if (my $unzip = which('unzip')) { $self->chat("You have $unzip\n"); $self->{_backends}{unzip} = sub { my($self, $zipfile) = @_; my @opt = $self->{verbose} ? () : ('-q'); my(undef, $root, @others) = `@{[ qs $unzip ]} -t @{[ qs $zipfile ]}` or return undef; FILE: { chomp $root; if ($root !~ s{^\s+testing:\s+([^/]+)/.*?\s+OK$}{$1}) { $root = shift(@others); redo FILE if $root; } } $self->run_command([ $unzip, @opt, $zipfile ]); return $root if -d $root; $self->diag_fail("Bad archive: '$root' $zipfile"); return undef; } } else { $self->{_backends}{unzip} = sub { eval { require Archive::Zip } or die "Failed to extract $_[1] - You need to have unzip or Archive::Zip installed.\n"; my($self, $file) = @_; my $zip = Archive::Zip->new(); my $status; $status = $zip->read($file); $self->diag_fail("Read of file '$file' failed") if $status != Archive::Zip::AZ_OK(); my @members = $zip->members(); for my $member ( @members ) { my $af = $member->fileName(); next if ($af =~ m!^(/|\.\./)!); $status = $member->extractToFileNamed( $af ); $self->diag_fail("Extracting of file 'af' from zipfile '$file' failed") if $status != Archive::Zip::AZ_OK(); } my ($root) = $zip->membersMatching( qr<^[^/]+/$> ); $root &&= $root->fileName; return -d $root ? $root : undef; }; } } sub mask_uri_passwords { my($self, @strings) = @_; s{ (https?://) ([^:/]+) : [^@/]+ @ }{$1$2:********@}gx for @strings; return @strings; } 1; __END__ =encoding utf-8 =head1 NAME Menlo::CLI::Compat - cpanm compatible CPAN installer =head1 SYNOPSIS use Menlo::CLI::Compat; my $app = Menlo::CLI::Compat->new; $app->parse_options(@ARGV); $app->run; =head1 DESCRIPTION Menlo::CLI::Compat is a port of App::cpanminus to Menlo, and provides a compatibility layer for users and clients to depend on the specific cpanm behaviors. =head1 SEE ALSO L, L =cut MENLO_CLI_COMPAT $fatpacked{"Menlo/Dependency.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_DEPENDENCY'; package Menlo::Dependency; use strict; use CPAN::Meta::Requirements; use Class::Tiny qw( module version type original_version dist mirror url ); sub BUILDARGS { my($class, $module, $version, $type) = @_; return { module => $module, version => $version, type => $type || 'requires', }; } sub from_prereqs { my($class, $prereqs, $phases, $types) = @_; my @deps; for my $type (@$types) { push @deps, $class->from_versions( $prereqs->merged_requirements($phases, [$type])->as_string_hash, $type, ); } return @deps; } sub from_versions { my($class, $versions, $type) = @_; my @deps; while (my($module, $version) = each %$versions) { push @deps, $class->new($module, $version, $type) } @deps; } sub merge_with { my($self, $requirements) = @_; # save the original requirement $self->original_version($self->version); # should it clone? not cloning means we upgrade root $requirements on our way eval { $requirements->add_string_requirement($self->module, $self->version); }; if ($@ =~ /illegal requirements/) { # Just give a warning then replace with the root requirements # so that later CPAN::Meta::Check can give a valid error warn sprintf("Can't merge requirements for %s: '%s' and '%s'", $self->module, $self->version, $requirements->requirements_for_module($self->module)); } $self->version( $requirements->requirements_for_module($self->module) ); } sub requires_version { my $self = shift; # original_version may be 0 if (defined $self->original_version) { return $self->original_version; } $self->version; } sub is_requirement { $_[0]->type eq 'requires'; } 1; MENLO_DEPENDENCY $fatpacked{"Menlo/Index/MetaCPAN.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_INDEX_METACPAN'; use 5.008001; use strict; use warnings; package Menlo::Index::MetaCPAN; # ABSTRACT: Search index via MetaCPAN # VERSION use parent 'CPAN::Common::Index'; use Class::Tiny qw/uri include_dev/; use Carp; use HTTP::Tinyish; use JSON::PP (); use Time::Local (); sub BUILD { my $self = shift; my $uri = $self->uri; $uri = "https://fastapi.metacpan.org/v1/download_url/" unless defined $uri; # ensure URI ends in '/' $uri =~ s{/?$}{/}; $self->uri($uri); return; } sub search_packages { my ( $self, $args ) = @_; Carp::croak("Argument to search_packages must be hash reference") unless ref $args eq 'HASH'; my $range; if ( $args->{version} ) { $range = "== $args->{version}"; } elsif ( $args->{version_range} ) { $range = $args->{version_range}; } my %query = ( ($self->include_dev ? (dev => 1) : ()), ($range ? (version => $range) : ()), ); my $query = join "&", map { "$_=" . $self->_uri_escape($query{$_}) } sort keys %query; my $uri = $self->uri . $args->{package} . ($query ? "?$query" : ""); my $res = HTTP::Tinyish->new->get($uri); return unless $res->{success}; my $dist_meta = eval { JSON::PP::decode_json($res->{content}) }; if ($dist_meta && $dist_meta->{download_url}) { (my $distfile = $dist_meta->{download_url}) =~ s!.+/authors/id/\w/\w\w/!!; return { package => $args->{package}, version => $dist_meta->{version}, uri => "cpan:///distfile/$distfile", download_uri => $self->_download_uri("http://cpan.metacpan.org", $distfile), }; } return; } sub _parse_date { my($self, $date) = @_; my @date = $date =~ /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)$/; Time::Local::timegm($date[5], $date[4], $date[3], $date[2], $date[1] - 1, $date[0] - 1900); } sub _uri_escape { my($self, $string) = @_; $string =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; $string; } sub _download_uri { my($self, $base, $distfile) = @_; join "/", $base, "authors/id", substr($distfile, 0, 1), substr($distfile, 0, 2), $distfile; } sub index_age { return time } # pretend always current sub search_authors { return } # not supported 1; =for Pod::Coverage attributes validate_attributes search_packages search_authors BUILD =head1 SYNOPSIS use CPAN::Common::Index::MetaCPAN; $index = CPAN::Common::Index::MetaCPAN->new({ include_dev => 1 }); $index->search_packages({ package => "Moose", version => "1.1" }); $index->search_packages({ package => "Moose", version_range => ">= 1.1, < 2" }); =head1 DESCRIPTION This module implements a CPAN::Common::Index that searches for packages against the MetaCPAN API. This backend supports searching modules with a version range (as specified in L) which is translated into MetaCPAN search query. There is also a support for I release search, by passing C parameter to the index object. The result may include an optional field C which suggests a specific mirror URL to download from, which can be C if the archive was deleted, or C if the release date is within 1 day (because some mirrors might not have synced it yet). There is no support for searching packages with a regular expression, nor searching authors. =cut # vim: ts=4 sts=4 sw=4 et: MENLO_INDEX_METACPAN $fatpacked{"Menlo/Index/MetaDB.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_INDEX_METADB'; use 5.008001; use strict; use warnings; package Menlo::Index::MetaDB; # ABSTRACT: Search index via CPAN MetaDB our $VERSION = "1.9019"; use parent 'CPAN::Common::Index'; use Class::Tiny qw/uri/; use Carp; use CPAN::Meta::YAML; use CPAN::Meta::Requirements; use HTTP::Tiny; sub BUILD { my $self = shift; my $uri = $self->uri; $uri = "http://cpanmetadb.plackperl.org/v1.0/" unless defined $uri; # ensure URI ends in '/' $uri =~ s{/?$}{/}; $self->uri($uri); return; } sub search_packages { my ( $self, $args ) = @_; Carp::croak("Argument to search_packages must be hash reference") unless ref $args eq 'HASH'; return unless exists $args->{package} && ref $args->{package} eq ''; my $mod = $args->{package}; if ($args->{version} || $args->{version_range}) { my $res = HTTP::Tiny->new->get( $self->uri . "history/$mod" ); return unless $res->{success}; my $range = defined $args->{version} ? "== $args->{version}" : $args->{version_range}; my $reqs = CPAN::Meta::Requirements->from_string_hash({ $mod => $range }); my @found; for my $line ( split /\r?\n/, $res->{content} ) { if ($line =~ /^$mod\s+(\S+)\s+(\S+)$/) { push @found, { version => $1, version_o => version::->parse($1), distfile => $2, }; } } return unless @found; $found[-1]->{latest} = 1; my $match; for my $try (sort { $b->{version_o} <=> $a->{version_o} } @found) { if ($reqs->accepts_module($mod => $try->{version_o})) { $match = $try, last; } } if ($match) { my $file = $match->{distfile}; $file =~ s{^./../}{}; # strip leading return { package => $mod, version => $match->{version}, uri => "cpan:///distfile/$file", ($match->{latest} ? () : (download_uri => "http://backpan.perl.org/authors/id/$match->{distfile}")), }; } } else { my $res = HTTP::Tiny->new->get( $self->uri . "package/$mod" ); return unless $res->{success}; if ( my $yaml = CPAN::Meta::YAML->read_string( $res->{content} ) ) { my $meta = $yaml->[0]; if ( $meta && $meta->{distfile} ) { my $file = $meta->{distfile}; $file =~ s{^./../}{}; # strip leading return { package => $mod, version => $meta->{version}, uri => "cpan:///distfile/$file", }; } } } return; } sub index_age { return time }; # pretend always current sub search_authors { return }; # not supported 1; =for Pod::Coverage attributes validate_attributes search_packages search_authors BUILD =head1 SYNOPSIS use CPAN::Common::Index::MetaDB; $index = CPAN::Common::Index::MetaDB->new; $index->search_packages({ package => "Moose" }); $index->search_packages({ package => "Moose", version_range => ">= 2.0" }); =head1 DESCRIPTION This module implements a CPAN::Common::Index that searches for packages against the same CPAN MetaDB API used by L. There is no support for advanced package queries or searching authors. It just takes a package name and returns the corresponding version and distribution. =cut # vim: ts=4 sts=4 sw=4 et: MENLO_INDEX_METADB $fatpacked{"Menlo/Index/Mirror.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_INDEX_MIRROR'; package Menlo::Index::Mirror; use strict; use parent qw(CPAN::Common::Index::Mirror); use Class::Tiny qw(fetcher); use File::Basename (); use File::Spec (); use URI (); our $HAS_IO_UNCOMPRESS_GUNZIP = eval { require IO::Uncompress::Gunzip }; my %INDICES = ( # mailrc => 'authors/01mailrc.txt.gz', packages => 'modules/02packages.details.txt.gz', ); sub refresh_index { my $self = shift; for my $file ( values %INDICES ) { my $remote = URI->new_abs( $file, $self->mirror ); $remote =~ s/\.gz$// unless $HAS_IO_UNCOMPRESS_GUNZIP; my $local = File::Spec->catfile( $self->cache, File::Basename::basename($file) ); $self->fetcher->($remote, $local) or Carp::croak( "Cannot fetch $remote to $local"); if ($HAS_IO_UNCOMPRESS_GUNZIP) { ( my $uncompressed = $local ) =~ s/\.gz$//; IO::Uncompress::Gunzip::gunzip( $local, $uncompressed ) or Carp::croak "gunzip failed: $IO::Uncompress::Gunzip::GunzipError\n"; } } } 1; MENLO_INDEX_MIRROR $fatpacked{"Menlo/Legacy.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_LEGACY'; package Menlo::Legacy; use strict; our $VERSION = '1.9022'; 1; __END__ =encoding utf-8 =head1 NAME Menlo::Legacy - Legacy internal and client support for Menlo =head1 DESCRIPTION Menlo::Legacy is a package to install L which is a compatibility library that implements the classic version of cpanminus internals and behavios. This is so that existing users of cpanm and API clients such as L, L and L) can rely on the stable features and specific behaviors of cpanm. This way Menlo can evolve and be refactored without the fear of breaking any downstream clients, including C itself. =head1 AUTHOR Tatsuhiko Miyagawa Emiyagawa@bulknews.netE =head1 COPYRIGHT Copyright 2018- Tatsuhiko Miyagawa =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L =cut MENLO_LEGACY $fatpacked{"Menlo/Util.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MENLO_UTIL'; package Menlo::Util; use strict; use Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(WIN32); use constant WIN32 => $^O eq 'MSWin32'; if (WIN32) { require Win32::ShellQuote; *shell_quote = \&Win32::ShellQuote::quote_native; } else { require String::ShellQuote; *shell_quote = \&String::ShellQuote::shell_quote_best_effort; } 1; MENLO_UTIL $fatpacked{"Module/CPANfile.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MODULE_CPANFILE'; package Module::CPANfile; use strict; use warnings; use Cwd; use Carp (); use Module::CPANfile::Environment; use Module::CPANfile::Requirement; our $VERSION = '1.1004'; BEGIN { if (${^TAINT}) { *untaint = sub { my $str = shift; ($str) = $str =~ /^(.+)$/s; $str; }; } else { *untaint = sub { $_[0] }; } } sub new { my($class, $file) = @_; bless {}, $class; } sub load { my($proto, $file) = @_; my $self = ref $proto ? $proto : $proto->new; $self->parse($file || _default_cpanfile()); $self; } sub save { my($self, $path) = @_; open my $out, ">", $path or die "$path: $!"; print {$out} $self->to_string; } sub parse { my($self, $file) = @_; my $code = do { open my $fh, "<", $file or die "$file: $!"; join '', <$fh>; }; $code = untaint $code; my $env = Module::CPANfile::Environment->new($file); $env->parse($code) or die $@; $self->{_mirrors} = $env->mirrors; $self->{_prereqs} = $env->prereqs; } sub from_prereqs { my($proto, $prereqs) = @_; my $self = $proto->new; $self->{_prereqs} = Module::CPANfile::Prereqs->from_cpan_meta($prereqs); $self; } sub mirrors { my $self = shift; $self->{_mirrors} || []; } sub features { my $self = shift; map $self->feature($_), $self->{_prereqs}->identifiers; } sub feature { my($self, $identifier) = @_; $self->{_prereqs}->feature($identifier); } sub prereq { shift->prereqs } sub prereqs { my $self = shift; $self->{_prereqs}->as_cpan_meta; } sub merged_requirements { my $self = shift; $self->{_prereqs}->merged_requirements; } sub effective_prereqs { my($self, $features) = @_; $self->prereqs_with(@{$features || []}); } sub prereqs_with { my($self, @feature_identifiers) = @_; my @others = map { $self->feature($_)->prereqs } @feature_identifiers; $self->prereqs->with_merged_prereqs(\@others); } sub prereq_specs { my $self = shift; $self->prereqs->as_string_hash; } sub prereq_for_module { my($self, $module) = @_; $self->{_prereqs}->find($module); } sub options_for_module { my($self, $module) = @_; my $prereq = $self->prereq_for_module($module) or return; $prereq->requirement->options; } sub merge_meta { my($self, $file, $version) = @_; require CPAN::Meta; $version ||= $file =~ /\.yml$/ ? '1.4' : '2'; my $prereq = $self->prereqs; my $meta = CPAN::Meta->load_file($file); my $prereqs_hash = $prereq->with_merged_prereqs($meta->effective_prereqs)->as_string_hash; my $struct = { %{$meta->as_struct}, prereqs => $prereqs_hash }; CPAN::Meta->new($struct)->save($file, { version => $version }); } sub _d($) { require Data::Dumper; chomp(my $value = Data::Dumper->new([$_[0]])->Terse(1)->Dump); $value; } sub _default_cpanfile { my $file = Cwd::abs_path('cpanfile'); untaint $file; } sub to_string { my($self, $include_empty) = @_; my $mirrors = $self->mirrors; my $prereqs = $self->prereq_specs; my $code = ''; $code .= $self->_dump_mirrors($mirrors); $code .= $self->_dump_prereqs($prereqs, $include_empty); for my $feature ($self->features) { $code .= "feature @{[ _d $feature->{identifier} ]}, @{[ _d $feature->{description} ]} => sub {\n"; $code .= $self->_dump_prereqs($feature->{prereqs}->as_string_hash, $include_empty, 4); $code .= "};\n\n"; } $code =~ s/\n+$/\n/s; $code; } sub _dump_mirrors { my($self, $mirrors) = @_; my $code = ""; for my $url (@$mirrors) { $code .= "mirror @{[ _d $url ]};\n"; } $code =~ s/\n+$/\n/s; $code; } sub _dump_prereqs { my($self, $prereqs, $include_empty, $base_indent) = @_; my $code = ''; for my $phase (qw(runtime configure build test develop)) { my $indent = $phase eq 'runtime' ? '' : ' '; $indent .= (' ' x ($base_indent || 0)); my($phase_code, $requirements); $phase_code .= "on $phase => sub {\n" unless $phase eq 'runtime'; for my $type (qw(requires recommends suggests conflicts)) { for my $mod (sort keys %{$prereqs->{$phase}{$type}}) { my $ver = $prereqs->{$phase}{$type}{$mod}; $phase_code .= $ver eq '0' ? "${indent}$type @{[ _d $mod ]}" : "${indent}$type @{[ _d $mod ]}, @{[ _d $ver ]}"; my $options = $self->options_for_module($mod) || {}; if (%$options) { my @opts; for my $key (keys %$options) { my $k = $key =~ /^[a-zA-Z0-9_]+$/ ? $key : _d $key; push @opts, "$k => @{[ _d $options->{$k} ]}"; } $phase_code .= ",\n" . join(",\n", map " $indent$_", @opts); } $phase_code .= ";\n"; $requirements++; } } $phase_code .= "\n" unless $requirements; $phase_code .= "};\n" unless $phase eq 'runtime'; $code .= $phase_code . "\n" if $requirements or $include_empty; } $code =~ s/\n+$/\n/s; $code; } 1; __END__ =head1 NAME Module::CPANfile - Parse cpanfile =head1 SYNOPSIS use Module::CPANfile; my $file = Module::CPANfile->load("cpanfile"); my $prereqs = $file->prereqs; # CPAN::Meta::Prereqs object my @features = $file->features; # CPAN::Meta::Feature objects my $merged_prereqs = $file->prereqs_with(@identifiers); # CPAN::Meta::Prereqs $file->merge_meta('MYMETA.json'); =head1 DESCRIPTION Module::CPANfile is a tool to handle L format to load application specific dependencies, not just for CPAN distributions. =head1 METHODS =over 4 =item load $file = Module::CPANfile->load; $file = Module::CPANfile->load('cpanfile'); Load and parse a cpanfile. By default it tries to load C in the current directory, unless you pass the path to its argument. =item from_prereqs $file = Module::CPANfile->from_prereqs({ runtime => { requires => { DBI => '1.000' } }, }); Creates a new Module::CPANfile object from prereqs hash you can get via L's C, or L' C. # read MYMETA, then feed the prereqs to create Module::CPANfile my $meta = CPAN::Meta->load_file('MYMETA.json'); my $file = Module::CPANfile->from_prereqs($meta->prereqs); # load cpanfile, then recreate it with round-trip my $file = Module::CPANfile->load('cpanfile'); $file = Module::CPANfile->from_prereqs($file->prereq_specs); # or $file->prereqs->as_string_hash =item prereqs Returns L object out of the parsed cpanfile. =item prereq_specs Returns a hash reference that should be passed to C<< CPAN::Meta::Prereqs->new >>. =item features Returns a list of features available in the cpanfile as L. =item prereqs_with(@identifiers), effective_prereqs(\@identifiers) Returns L object, with merged prereqs for features identified with the C<@identifiers>. =item to_string($include_empty) $file->to_string; $file->to_string(1); Returns a canonical string (code) representation for cpanfile. Useful if you want to convert L to a new cpanfile. # read MYMETA's prereqs and print cpanfile representation of it my $meta = CPAN::Meta->load_file('MYMETA.json'); my $file = Module::CPANfile->from_prereqs($meta->prereqs); print $file->to_string; By default, it omits the phase where there're no modules registered. If you pass the argument of a true value, it will print them as well. =item save $file->save('cpanfile'); Saves the currently loaded prereqs as a new C by calling C. Beware B. Taking a backup or giving warnings to users is a caller's responsibility. # Read MYMETA.json and creates a new cpanfile my $meta = CPAN::Meta->load_file('MYMETA.json'); my $file = Module::CPANfile->from_prereqs($meta->prereqs); $file->save('cpanfile'); =item merge_meta $file->merge_meta('META.yml'); $file->merge_meta('MYMETA.json', '2.0'); Merge the effective prereqs with Meta specification loaded from the given META file, using CPAN::Meta. You can specify the META spec version in the second argument, which defaults to 1.4 in case the given file is YAML, and 2 if it is JSON. =item options_for_module my $options = $file->options_for_module($module); Returns the extra options specified for a given module as a hash reference. Returns C when the given module is not specified in the C. For example, # cpanfile requires 'Plack', '1.000', dist => "MIYAGAWA/Plack-1.000.tar.gz"; # ... my $file = Module::CPANfile->load; my $options = $file->options_for_module('Plack'); # => { dist => "MIYAGAWA/Plack-1.000.tar.gz" } =back =head1 AUTHOR Tatsuhiko Miyagawa =head1 SEE ALSO L, L, L =cut MODULE_CPANFILE $fatpacked{"Module/CPANfile/Environment.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MODULE_CPANFILE_ENVIRONMENT'; package Module::CPANfile::Environment; use strict; use warnings; use Module::CPANfile::Prereqs; use Carp (); my @bindings = qw( on requires recommends suggests conflicts feature osname mirror configure_requires build_requires test_requires author_requires ); my $file_id = 1; sub new { my($class, $file) = @_; bless { file => $file, phase => 'runtime', # default phase feature => undef, features => {}, prereqs => Module::CPANfile::Prereqs->new, mirrors => [], }, $class; } sub bind { my $self = shift; my $pkg = caller; for my $binding (@bindings) { no strict 'refs'; *{"$pkg\::$binding"} = sub { $self->$binding(@_) }; } } sub parse { my($self, $code) = @_; my $err; { local $@; $file_id++; $self->_evaluate(<bind } # line 1 "$self->{file}" $code; EVAL $err = $@; } if ($err) { die "Parsing $self->{file} failed: $err" }; return 1; } sub _evaluate { my $_environment = $_[0]; eval $_[1]; } sub prereqs { $_[0]->{prereqs} } sub mirrors { $_[0]->{mirrors} } # DSL goes from here sub on { my($self, $phase, $code) = @_; local $self->{phase} = $phase; $code->(); } sub feature { my($self, $identifier, $description, $code) = @_; # shortcut: feature identifier => sub { ... } if (@_ == 3 && ref($description) eq 'CODE') { $code = $description; $description = $identifier; } unless (ref $description eq '' && ref $code eq 'CODE') { Carp::croak("Usage: feature 'identifier', 'Description' => sub { ... }"); } local $self->{feature} = $identifier; $self->prereqs->add_feature($identifier, $description); $code->(); } sub osname { die "TODO" } sub mirror { my($self, $url) = @_; push @{$self->{mirrors}}, $url; } sub requirement_for { my($self, $module, @args) = @_; my $requirement = 0; $requirement = shift @args if @args % 2; return Module::CPANfile::Requirement->new( name => $module, version => $requirement, @args, ); } sub requires { my $self = shift; $self->add_prereq(requires => @_); } sub recommends { my $self = shift; $self->add_prereq(recommends => @_); } sub suggests { my $self = shift; $self->add_prereq(suggests => @_); } sub conflicts { my $self = shift; $self->add_prereq(conflicts => @_); } sub add_prereq { my($self, $type, $module, @args) = @_; $self->prereqs->add( feature => $self->{feature}, phase => $self->{phase}, type => $type, module => $module, requirement => $self->requirement_for($module, @args), ); } # Module::Install compatible shortcuts sub configure_requires { my($self, @args) = @_; $self->on(configure => sub { $self->requires(@args) }); } sub build_requires { my($self, @args) = @_; $self->on(build => sub { $self->requires(@args) }); } sub test_requires { my($self, @args) = @_; $self->on(test => sub { $self->requires(@args) }); } sub author_requires { my($self, @args) = @_; $self->on(develop => sub { $self->requires(@args) }); } 1; MODULE_CPANFILE_ENVIRONMENT $fatpacked{"Module/CPANfile/Prereq.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MODULE_CPANFILE_PREREQ'; package Module::CPANfile::Prereq; use strict; sub new { my($class, %options) = @_; bless \%options, $class; } sub feature { $_[0]->{feature} } sub phase { $_[0]->{phase} } sub type { $_[0]->{type} } sub module { $_[0]->{module} } sub requirement { $_[0]->{requirement} } 1; MODULE_CPANFILE_PREREQ $fatpacked{"Module/CPANfile/Prereqs.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MODULE_CPANFILE_PREREQS'; package Module::CPANfile::Prereqs; use strict; use Carp (); use CPAN::Meta::Feature; use Module::CPANfile::Prereq; sub from_cpan_meta { my($class, $prereqs) = @_; my $self = $class->new; for my $phase (keys %$prereqs) { for my $type (keys %{ $prereqs->{$phase} }) { while (my($module, $requirement) = each %{ $prereqs->{$phase}{$type} }) { $self->add( phase => $phase, type => $type, module => $module, requirement => Module::CPANfile::Requirement->new(name => $module, version => $requirement), ); } } } $self; } sub new { my $class = shift; bless { prereqs => {}, features => {}, }, $class; } sub add_feature { my($self, $identifier, $description) = @_; $self->{features}{$identifier} = { description => $description }; } sub add { my($self, %args) = @_; my $feature = $args{feature} || ''; push @{$self->{prereqs}{$feature}}, Module::CPANfile::Prereq->new(%args); } sub as_cpan_meta { my $self = shift; $self->{cpanmeta} ||= $self->build_cpan_meta; } sub build_cpan_meta { my($self, $feature) = @_; CPAN::Meta::Prereqs->new($self->specs($feature)); } sub specs { my($self, $feature) = @_; $feature = '' unless defined $feature; my $prereqs = $self->{prereqs}{$feature} || []; my $specs = {}; for my $prereq (@$prereqs) { $specs->{$prereq->phase}{$prereq->type}{$prereq->module} = $prereq->requirement->version; } return $specs; } sub merged_requirements { my $self = shift; my $reqs = CPAN::Meta::Requirements->new; for my $prereq (@{$self->{prereqs}}) { $reqs->add_string_requirement($prereq->module, $prereq->requirement->version); } $reqs; } sub find { my($self, $module) = @_; for my $feature ('', keys %{$self->{features}}) { for my $prereq (@{$self->{prereqs}{$feature}}) { return $prereq if $prereq->module eq $module; } } return; } sub identifiers { my $self = shift; keys %{$self->{features}}; } sub feature { my($self, $identifier) = @_; my $data = $self->{features}{$identifier} or Carp::croak("Unknown feature '$identifier'"); my $prereqs = $self->build_cpan_meta($identifier); CPAN::Meta::Feature->new($identifier, { description => $data->{description}, prereqs => $prereqs->as_string_hash, }); } 1; MODULE_CPANFILE_PREREQS $fatpacked{"Module/CPANfile/Requirement.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MODULE_CPANFILE_REQUIREMENT'; package Module::CPANfile::Requirement; use strict; sub new { my ($class, %args) = @_; $args{version} ||= 0; bless +{ name => delete $args{name}, version => delete $args{version}, options => \%args, }, $class; } sub name { $_[0]->{name} } sub version { $_[0]->{version} } sub options { $_[0]->{options} } sub has_options { keys %{$_[0]->{options}} > 0; } 1; MODULE_CPANFILE_REQUIREMENT $fatpacked{"Module/Metadata.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'MODULE_METADATA'; # -*- mode: cperl; tab-width: 8; indent-tabs-mode: nil; basic-offset: 2 -*- # vim:ts=8:sw=2:et:sta:sts=2:tw=78 package Module::Metadata; # git description: v1.000035-3-gaa51be1 # ABSTRACT: Gather package and POD information from perl module files # Adapted from Perl-licensed code originally distributed with # Module-Build by Ken Williams # This module provides routines to gather information about # perl modules (assuming this may be expanded in the distant # parrot future to look at other types of modules). sub __clean_eval { eval $_[0] } use strict; use warnings; our $VERSION = '1.000036'; use Carp qw/croak/; use File::Spec; BEGIN { # Try really hard to not depend ony any DynaLoaded module, such as IO::File or Fcntl eval { require Fcntl; Fcntl->import('SEEK_SET'); 1; } or *SEEK_SET = sub { 0 } } use version 0.87; BEGIN { if ($INC{'Log/Contextual.pm'}) { require "Log/Contextual/WarnLogger.pm"; # Hide from AutoPrereqs Log::Contextual->import('log_info', '-default_logger' => Log::Contextual::WarnLogger->new({ env_prefix => 'MODULE_METADATA', }), ); } else { *log_info = sub (&) { warn $_[0]->() }; } } use File::Find qw(find); my $V_NUM_REGEXP = qr{v?[0-9._]+}; # crudely, a v-string or decimal my $PKG_FIRST_WORD_REGEXP = qr{ # the FIRST word in a package name [a-zA-Z_] # the first word CANNOT start with a digit (?: [\w']? # can contain letters, digits, _, or ticks \w # But, NO multi-ticks or trailing ticks )* }x; my $PKG_ADDL_WORD_REGEXP = qr{ # the 2nd+ word in a package name \w # the 2nd+ word CAN start with digits (?: [\w']? # and can contain letters or ticks \w # But, NO multi-ticks or trailing ticks )* }x; my $PKG_NAME_REGEXP = qr{ # match a package name (?: :: )? # a pkg name can start with arisdottle $PKG_FIRST_WORD_REGEXP # a package word (?: (?: :: )+ ### arisdottle (allow one or many times) $PKG_ADDL_WORD_REGEXP ### a package word )* # ^ zero, one or many times (?: :: # allow trailing arisdottle )? }x; my $PKG_REGEXP = qr{ # match a package declaration ^[\s\{;]* # intro chars on a line package # the word 'package' \s+ # whitespace ($PKG_NAME_REGEXP) # a package name \s* # optional whitespace ($V_NUM_REGEXP)? # optional version number \s* # optional whitesapce [;\{] # semicolon line terminator or block start (since 5.16) }x; my $VARNAME_REGEXP = qr{ # match fully-qualified VERSION name ([\$*]) # sigil - $ or * ( ( # optional leading package name (?:::|\')? # possibly starting like just :: (a la $::VERSION) (?:\w+(?:::|\'))* # Foo::Bar:: ... )? VERSION )\b }x; my $VERS_REGEXP = qr{ # match a VERSION definition (?: \(\s*$VARNAME_REGEXP\s*\) # with parens | $VARNAME_REGEXP # without parens ) \s* =[^=~>] # = but not ==, nor =~, nor => }x; sub new_from_file { my $class = shift; my $filename = File::Spec->rel2abs( shift ); return undef unless defined( $filename ) && -f $filename; return $class->_init(undef, $filename, @_); } sub new_from_handle { my $class = shift; my $handle = shift; my $filename = shift; return undef unless defined($handle) && defined($filename); $filename = File::Spec->rel2abs( $filename ); return $class->_init(undef, $filename, @_, handle => $handle); } sub new_from_module { my $class = shift; my $module = shift; my %props = @_; $props{inc} ||= \@INC; my $filename = $class->find_module_by_name( $module, $props{inc} ); return undef unless defined( $filename ) && -f $filename; return $class->_init($module, $filename, %props); } { my $compare_versions = sub { my ($v1, $op, $v2) = @_; $v1 = version->new($v1) unless UNIVERSAL::isa($v1,'version'); my $eval_str = "\$v1 $op \$v2"; my $result = eval $eval_str; log_info { "error comparing versions: '$eval_str' $@" } if $@; return $result; }; my $normalize_version = sub { my ($version) = @_; if ( $version =~ /[=<>!,]/ ) { # logic, not just version # take as is without modification } elsif ( ref $version eq 'version' ) { # version objects $version = $version->is_qv ? $version->normal : $version->stringify; } elsif ( $version =~ /^[^v][^.]*\.[^.]+\./ ) { # no leading v, multiple dots # normalize string tuples without "v": "1.2.3" -> "v1.2.3" $version = "v$version"; } else { # leave alone } return $version; }; # separate out some of the conflict resolution logic my $resolve_module_versions = sub { my $packages = shift; my( $file, $version ); my $err = ''; foreach my $p ( @$packages ) { if ( defined( $p->{version} ) ) { if ( defined( $version ) ) { if ( $compare_versions->( $version, '!=', $p->{version} ) ) { $err .= " $p->{file} ($p->{version})\n"; } else { # same version declared multiple times, ignore } } else { $file = $p->{file}; $version = $p->{version}; } } $file ||= $p->{file} if defined( $p->{file} ); } if ( $err ) { $err = " $file ($version)\n" . $err; } my %result = ( file => $file, version => $version, err => $err ); return \%result; }; sub provides { my $class = shift; croak "provides() requires key/value pairs \n" if @_ % 2; my %args = @_; croak "provides() takes only one of 'dir' or 'files'\n" if $args{dir} && $args{files}; croak "provides() requires a 'version' argument" unless defined $args{version}; croak "provides() does not support version '$args{version}' metadata" unless grep $args{version} eq $_, qw/1.4 2/; $args{prefix} = 'lib' unless defined $args{prefix}; my $p; if ( $args{dir} ) { $p = $class->package_versions_from_directory($args{dir}); } else { croak "provides() requires 'files' to be an array reference\n" unless ref $args{files} eq 'ARRAY'; $p = $class->package_versions_from_directory($args{files}); } # Now, fix up files with prefix if ( length $args{prefix} ) { # check in case disabled with q{} $args{prefix} =~ s{/$}{}; for my $v ( values %$p ) { $v->{file} = "$args{prefix}/$v->{file}"; } } return $p } sub package_versions_from_directory { my ( $class, $dir, $files ) = @_; my @files; if ( $files ) { @files = @$files; } else { find( { wanted => sub { push @files, $_ if -f $_ && /\.pm$/; }, no_chdir => 1, }, $dir ); } # First, we enumerate all packages & versions, # separating into primary & alternative candidates my( %prime, %alt ); foreach my $file (@files) { my $mapped_filename = File::Spec->abs2rel( $file, $dir ); my @path = File::Spec->splitdir( $mapped_filename ); (my $prime_package = join( '::', @path )) =~ s/\.pm$//; my $pm_info = $class->new_from_file( $file ); foreach my $package ( $pm_info->packages_inside ) { next if $package eq 'main'; # main can appear numerous times, ignore next if $package eq 'DB'; # special debugging package, ignore next if grep /^_/, split( /::/, $package ); # private package, ignore my $version = $pm_info->version( $package ); $prime_package = $package if lc($prime_package) eq lc($package); if ( $package eq $prime_package ) { if ( exists( $prime{$package} ) ) { croak "Unexpected conflict in '$package'; multiple versions found.\n"; } else { $mapped_filename = "$package.pm" if lc("$package.pm") eq lc($mapped_filename); $prime{$package}{file} = $mapped_filename; $prime{$package}{version} = $version if defined( $version ); } } else { push( @{$alt{$package}}, { file => $mapped_filename, version => $version, } ); } } } # Then we iterate over all the packages found above, identifying conflicts # and selecting the "best" candidate for recording the file & version # for each package. foreach my $package ( keys( %alt ) ) { my $result = $resolve_module_versions->( $alt{$package} ); if ( exists( $prime{$package} ) ) { # primary package selected if ( $result->{err} ) { # Use the selected primary package, but there are conflicting # errors among multiple alternative packages that need to be # reported log_info { "Found conflicting versions for package '$package'\n" . " $prime{$package}{file} ($prime{$package}{version})\n" . $result->{err} }; } elsif ( defined( $result->{version} ) ) { # There is a primary package selected, and exactly one # alternative package if ( exists( $prime{$package}{version} ) && defined( $prime{$package}{version} ) ) { # Unless the version of the primary package agrees with the # version of the alternative package, report a conflict if ( $compare_versions->( $prime{$package}{version}, '!=', $result->{version} ) ) { log_info { "Found conflicting versions for package '$package'\n" . " $prime{$package}{file} ($prime{$package}{version})\n" . " $result->{file} ($result->{version})\n" }; } } else { # The prime package selected has no version so, we choose to # use any alternative package that does have a version $prime{$package}{file} = $result->{file}; $prime{$package}{version} = $result->{version}; } } else { # no alt package found with a version, but we have a prime # package so we use it whether it has a version or not } } else { # No primary package was selected, use the best alternative if ( $result->{err} ) { log_info { "Found conflicting versions for package '$package'\n" . $result->{err} }; } # Despite possible conflicting versions, we choose to record # something rather than nothing $prime{$package}{file} = $result->{file}; $prime{$package}{version} = $result->{version} if defined( $result->{version} ); } } # Normalize versions. Can't use exists() here because of bug in YAML::Node. # XXX "bug in YAML::Node" comment seems irrelevant -- dagolden, 2009-05-18 for (grep defined $_->{version}, values %prime) { $_->{version} = $normalize_version->( $_->{version} ); } return \%prime; } } sub _init { my $class = shift; my $module = shift; my $filename = shift; my %props = @_; my $handle = delete $props{handle}; my( %valid_props, @valid_props ); @valid_props = qw( collect_pod inc ); @valid_props{@valid_props} = delete( @props{@valid_props} ); warn "Unknown properties: @{[keys %props]}\n" if scalar( %props ); my %data = ( module => $module, filename => $filename, version => undef, packages => [], versions => {}, pod => {}, pod_headings => [], collect_pod => 0, %valid_props, ); my $self = bless(\%data, $class); if ( not $handle ) { my $filename = $self->{filename}; open $handle, '<', $filename or croak( "Can't open '$filename': $!" ); $self->_handle_bom($handle, $filename); } $self->_parse_fh($handle); @{$self->{packages}} = __uniq(@{$self->{packages}}); unless($self->{module} and length($self->{module})) { # CAVEAT (possible TODO): .pmc files not treated the same as .pm if ($self->{filename} =~ /\.pm$/) { my ($v, $d, $f) = File::Spec->splitpath($self->{filename}); $f =~ s/\..+$//; my @candidates = grep /(^|::)$f$/, @{$self->{packages}}; $self->{module} = shift(@candidates); # this may be undef } else { # this seems like an atrocious heuristic, albeit marginally better than # what was here before. It should be rewritten entirely to be more like # "if it's not a .pm file, it's not require()able as a name, therefore # name() should be undef." if ((grep /main/, @{$self->{packages}}) or (grep /main/, keys %{$self->{versions}})) { $self->{module} = 'main'; } else { # TODO: this should maybe default to undef instead $self->{module} = $self->{packages}[0] || ''; } } } $self->{version} = $self->{versions}{$self->{module}} if defined( $self->{module} ); return $self; } # class method sub _do_find_module { my $class = shift; my $module = shift || croak 'find_module_by_name() requires a package name'; my $dirs = shift || \@INC; my $file = File::Spec->catfile(split( /::/, $module)); foreach my $dir ( @$dirs ) { my $testfile = File::Spec->catfile($dir, $file); return [ File::Spec->rel2abs( $testfile ), $dir ] if -e $testfile and !-d _; # For stuff like ExtUtils::xsubpp # CAVEAT (possible TODO): .pmc files are not discoverable here $testfile .= '.pm'; return [ File::Spec->rel2abs( $testfile ), $dir ] if -e $testfile; } return; } # class method sub find_module_by_name { my $found = shift()->_do_find_module(@_) or return; return $found->[0]; } # class method sub find_module_dir_by_name { my $found = shift()->_do_find_module(@_) or return; return $found->[1]; } # given a line of perl code, attempt to parse it if it looks like a # $VERSION assignment, returning sigil, full name, & package name sub _parse_version_expression { my $self = shift; my $line = shift; my( $sigil, $variable_name, $package); if ( $line =~ /$VERS_REGEXP/o ) { ( $sigil, $variable_name, $package) = $2 ? ( $1, $2, $3 ) : ( $4, $5, $6 ); if ( $package ) { $package = ($package eq '::') ? 'main' : $package; $package =~ s/::$//; } } return ( $sigil, $variable_name, $package ); } # Look for a UTF-8/UTF-16BE/UTF-16LE BOM at the beginning of the stream. # If there's one, then skip it and set the :encoding layer appropriately. sub _handle_bom { my ($self, $fh, $filename) = @_; my $pos = tell $fh; return unless defined $pos; my $buf = ' ' x 2; my $count = read $fh, $buf, length $buf; return unless defined $count and $count >= 2; my $encoding; if ( $buf eq "\x{FE}\x{FF}" ) { $encoding = 'UTF-16BE'; } elsif ( $buf eq "\x{FF}\x{FE}" ) { $encoding = 'UTF-16LE'; } elsif ( $buf eq "\x{EF}\x{BB}" ) { $buf = ' '; $count = read $fh, $buf, length $buf; if ( defined $count and $count >= 1 and $buf eq "\x{BF}" ) { $encoding = 'UTF-8'; } } if ( defined $encoding ) { if ( "$]" >= 5.008 ) { binmode( $fh, ":encoding($encoding)" ); } } else { seek $fh, $pos, SEEK_SET or croak( sprintf "Can't reset position to the top of '$filename'" ); } return $encoding; } sub _parse_fh { my ($self, $fh) = @_; my( $in_pod, $seen_end, $need_vers ) = ( 0, 0, 0 ); my( @packages, %vers, %pod, @pod ); my $package = 'main'; my $pod_sect = ''; my $pod_data = ''; my $in_end = 0; while (defined( my $line = <$fh> )) { my $line_num = $.; chomp( $line ); # From toke.c : any line that begins by "=X", where X is an alphabetic # character, introduces a POD segment. my $is_cut; if ( $line =~ /^=([a-zA-Z].*)/ ) { my $cmd = $1; # Then it goes back to Perl code for "=cutX" where X is a non-alphabetic # character (which includes the newline, but here we chomped it away). $is_cut = $cmd =~ /^cut(?:[^a-zA-Z]|$)/; $in_pod = !$is_cut; } if ( $in_pod ) { if ( $line =~ /^=head[1-4]\s+(.+)\s*$/ ) { push( @pod, $1 ); if ( $self->{collect_pod} && length( $pod_data ) ) { $pod{$pod_sect} = $pod_data; $pod_data = ''; } $pod_sect = $1; } elsif ( $self->{collect_pod} ) { $pod_data .= "$line\n"; } next; } elsif ( $is_cut ) { if ( $self->{collect_pod} && length( $pod_data ) ) { $pod{$pod_sect} = $pod_data; $pod_data = ''; } $pod_sect = ''; next; } # Skip after __END__ next if $in_end; # Skip comments in code next if $line =~ /^\s*#/; # Would be nice if we could also check $in_string or something too if ($line eq '__END__') { $in_end++; next; } last if $line eq '__DATA__'; # parse $line to see if it's a $VERSION declaration my( $version_sigil, $version_fullname, $version_package ) = index($line, 'VERSION') >= 1 ? $self->_parse_version_expression( $line ) : (); if ( $line =~ /$PKG_REGEXP/o ) { $package = $1; my $version = $2; push( @packages, $package ) unless grep( $package eq $_, @packages ); $need_vers = defined $version ? 0 : 1; if ( not exists $vers{$package} and defined $version ){ # Upgrade to a version object. my $dwim_version = eval { _dwim_version($version) }; croak "Version '$version' from $self->{filename} does not appear to be valid:\n$line\n\nThe fatal error was: $@\n" unless defined $dwim_version; # "0" is OK! $vers{$package} = $dwim_version; } } # VERSION defined with full package spec, i.e. $Module::VERSION elsif ( $version_fullname && $version_package ) { # we do NOT save this package in found @packages $need_vers = 0 if $version_package eq $package; unless ( defined $vers{$version_package} && length $vers{$version_package} ) { $vers{$version_package} = $self->_evaluate_version_line( $version_sigil, $version_fullname, $line ); } } # first non-comment line in undeclared package main is VERSION elsif ( $package eq 'main' && $version_fullname && !exists($vers{main}) ) { $need_vers = 0; my $v = $self->_evaluate_version_line( $version_sigil, $version_fullname, $line ); $vers{$package} = $v; push( @packages, 'main' ); } # first non-comment line in undeclared package defines package main elsif ( $package eq 'main' && !exists($vers{main}) && $line =~ /\w/ ) { $need_vers = 1; $vers{main} = ''; push( @packages, 'main' ); } # only keep if this is the first $VERSION seen elsif ( $version_fullname && $need_vers ) { $need_vers = 0; my $v = $self->_evaluate_version_line( $version_sigil, $version_fullname, $line ); unless ( defined $vers{$package} && length $vers{$package} ) { $vers{$package} = $v; } } } # end loop over each line if ( $self->{collect_pod} && length($pod_data) ) { $pod{$pod_sect} = $pod_data; } $self->{versions} = \%vers; $self->{packages} = \@packages; $self->{pod} = \%pod; $self->{pod_headings} = \@pod; } sub __uniq (@) { my (%seen, $key); grep !$seen{ $key = $_ }++, @_; } { my $pn = 0; sub _evaluate_version_line { my $self = shift; my( $sigil, $variable_name, $line ) = @_; # We compile into a local sub because 'use version' would cause # compiletime/runtime issues with local() $pn++; # everybody gets their own package my $eval = qq{ my \$dummy = q# Hide from _packages_inside() #; package Module::Metadata::_version::p${pn}; use version; sub { local $sigil$variable_name; $line; return \$$variable_name if defined \$$variable_name; return \$Module::Metadata::_version::p${pn}::$variable_name; }; }; $eval = $1 if $eval =~ m{^(.+)}s; local $^W; # Try to get the $VERSION my $vsub = __clean_eval($eval); # some modules say $VERSION $Foo::Bar::VERSION, but Foo::Bar isn't # installed, so we need to hunt in ./lib for it if ( $@ =~ /Can't locate/ && -d 'lib' ) { local @INC = ('lib',@INC); $vsub = __clean_eval($eval); } warn "Error evaling version line '$eval' in $self->{filename}: $@\n" if $@; (ref($vsub) eq 'CODE') or croak "failed to build version sub for $self->{filename}"; my $result = eval { $vsub->() }; # FIXME: $eval is not the right thing to print here croak "Could not get version from $self->{filename} by executing:\n$eval\n\nThe fatal error was: $@\n" if $@; # Upgrade it into a version object my $version = eval { _dwim_version($result) }; # FIXME: $eval is not the right thing to print here croak "Version '$result' from $self->{filename} does not appear to be valid:\n$eval\n\nThe fatal error was: $@\n" unless defined $version; # "0" is OK! return $version; } } # Try to DWIM when things fail the lax version test in obvious ways { my @version_prep = ( # Best case, it just works sub { return shift }, # If we still don't have a version, try stripping any # trailing junk that is prohibited by lax rules sub { my $v = shift; $v =~ s{([0-9])[a-z-].*$}{$1}i; # 1.23-alpha or 1.23b return $v; }, # Activestate apparently creates custom versions like '1.23_45_01', which # cause version.pm to think it's an invalid alpha. So check for that # and strip them sub { my $v = shift; my $num_dots = () = $v =~ m{(\.)}g; my $num_unders = () = $v =~ m{(_)}g; my $leading_v = substr($v,0,1) eq 'v'; if ( ! $leading_v && $num_dots < 2 && $num_unders > 1 ) { $v =~ s{_}{}g; $num_unders = () = $v =~ m{(_)}g; } return $v; }, # Worst case, try numifying it like we would have before version objects sub { my $v = shift; no warnings 'numeric'; return 0 + $v; }, ); sub _dwim_version { my ($result) = shift; return $result if ref($result) eq 'version'; my ($version, $error); for my $f (@version_prep) { $result = $f->($result); $version = eval { version->new($result) }; $error ||= $@ if $@; # capture first failure last if defined $version; } croak $error unless defined $version; return $version; } } ############################################################ # accessors sub name { $_[0]->{module} } sub filename { $_[0]->{filename} } sub packages_inside { @{$_[0]->{packages}} } sub pod_inside { @{$_[0]->{pod_headings}} } sub contains_pod { 0+@{$_[0]->{pod_headings}} } sub version { my $self = shift; my $mod = shift || $self->{module}; my $vers; if ( defined( $mod ) && length( $mod ) && exists( $self->{versions}{$mod} ) ) { return $self->{versions}{$mod}; } else { return undef; } } sub pod { my $self = shift; my $sect = shift; if ( defined( $sect ) && length( $sect ) && exists( $self->{pod}{$sect} ) ) { return $self->{pod}{$sect}; } else { return undef; } } sub is_indexable { my ($self, $package) = @_; my @indexable_packages = grep $_ ne 'main', $self->packages_inside; # check for specific package, if provided return !! grep $_ eq $package, @indexable_packages if $package; # otherwise, check for any indexable packages at all return !! @indexable_packages; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Module::Metadata - Gather package and POD information from perl module files =head1 VERSION version 1.000036 =head1 SYNOPSIS use Module::Metadata; # information about a .pm file my $info = Module::Metadata->new_from_file( $file ); my $version = $info->version; # CPAN META 'provides' field for .pm files in a directory my $provides = Module::Metadata->provides( dir => 'lib', version => 2 ); =head1 DESCRIPTION This module provides a standard way to gather metadata about a .pm file through (mostly) static analysis and (some) code execution. When determining the version of a module, the C<$VERSION> assignment is Ced, as is traditional in the CPAN toolchain. =head1 CLASS METHODS =head2 C<< new_from_file($filename, collect_pod => 1) >> Constructs a C object given the path to a file. Returns undef if the filename does not exist. C is a optional boolean argument that determines whether POD data is collected and stored for reference. POD data is not collected by default. POD headings are always collected. If the file begins by an UTF-8, UTF-16BE or UTF-16LE byte-order mark, then it is skipped before processing, and the content of the file is also decoded appropriately starting from perl 5.8. =head2 C<< new_from_handle($handle, $filename, collect_pod => 1) >> This works just like C, except that a handle can be provided as the first argument. Note that there is no validation to confirm that the handle is a handle or something that can act like one. Passing something that isn't a handle will cause a exception when trying to read from it. The C argument is mandatory or undef will be returned. You are responsible for setting the decoding layers on C<$handle> if required. =head2 C<< new_from_module($module, collect_pod => 1, inc => \@dirs) >> Constructs a C object given a module or package name. Returns undef if the module cannot be found. In addition to accepting the C argument as described above, this method accepts a C argument which is a reference to an array of directories to search for the module. If none are given, the default is @INC. If the file that contains the module begins by an UTF-8, UTF-16BE or UTF-16LE byte-order mark, then it is skipped before processing, and the content of the file is also decoded appropriately starting from perl 5.8. =head2 C<< find_module_by_name($module, \@dirs) >> Returns the path to a module given the module or package name. A list of directories can be passed in as an optional parameter, otherwise @INC is searched. Can be called as either an object or a class method. =head2 C<< find_module_dir_by_name($module, \@dirs) >> Returns the entry in C<@dirs> (or C<@INC> by default) that contains the module C<$module>. A list of directories can be passed in as an optional parameter, otherwise @INC is searched. Can be called as either an object or a class method. =head2 C<< provides( %options ) >> This is a convenience wrapper around C to generate a CPAN META C data structure. It takes key/value pairs. Valid option keys include: =over =item version B<(required)> Specifies which version of the L should be used as the format of the C output. Currently only '1.4' and '2' are supported (and their format is identical). This may change in the future as the definition of C changes. The C option is required. If it is omitted or if an unsupported version is given, then C will throw an error. =item dir Directory to search recursively for F<.pm> files. May not be specified with C. =item files Array reference of files to examine. May not be specified with C. =item prefix String to prepend to the C field of the resulting output. This defaults to F, which is the common case for most CPAN distributions with their F<.pm> files in F. This option ensures the META information has the correct relative path even when the C or C arguments are absolute or have relative paths from a location other than the distribution root. =back For example, given C of 'lib' and C of 'lib', the return value is a hashref of the form: { 'Package::Name' => { version => '0.123', file => 'lib/Package/Name.pm' }, 'OtherPackage::Name' => ... } =head2 C<< package_versions_from_directory($dir, \@files?) >> Scans C<$dir> for .pm files (unless C<@files> is given, in which case looks for those files in C<$dir> - and reads each file for packages and versions, returning a hashref of the form: { 'Package::Name' => { version => '0.123', file => 'Package/Name.pm' }, 'OtherPackage::Name' => ... } The C and C
packages are always omitted, as are any "private" packages that have leading underscores in the namespace (e.g. C) Note that the file path is relative to C<$dir> if that is specified. This B be used directly for CPAN META C. See the C method instead. =head2 C<< log_info (internal) >> Used internally to perform logging; imported from Log::Contextual if Log::Contextual has already been loaded, otherwise simply calls warn. =head1 OBJECT METHODS =head2 C<< name() >> Returns the name of the package represented by this module. If there is more than one package, it makes a best guess based on the filename. If it's a script (i.e. not a *.pm) the package name is 'main'. =head2 C<< version($package) >> Returns the version as defined by the $VERSION variable for the package as returned by the C method if no arguments are given. If given the name of a package it will attempt to return the version of that package if it is specified in the file. =head2 C<< filename() >> Returns the absolute path to the file. Note that this file may not actually exist on disk yet, e.g. if the module was read from an in-memory filehandle. =head2 C<< packages_inside() >> Returns a list of packages. Note: this is a raw list of packages discovered (or assumed, in the case of C
). It is not filtered for C, C
or private packages the way the C method does. Invalid package names are not returned, for example "Foo:Bar". Strange but valid package names are returned, for example "Foo::Bar::", and are left up to the caller on how to handle. =head2 C<< pod_inside() >> Returns a list of POD sections. =head2 C<< contains_pod() >> Returns true if there is any POD in the file. =head2 C<< pod($section) >> Returns the POD data in the given section. =head2 C<< is_indexable($package) >> or C<< is_indexable() >> Available since version 1.000020. Returns a boolean indicating whether the package (if provided) or any package (otherwise) is eligible for indexing by PAUSE, the Perl Authors Upload Server. Note This only checks for valid C declarations, and does not take any ownership information into account. =head1 SUPPORT Bugs may be submitted through L (or L). There is also a mailing list available for users of this distribution, at L. There is also an irc channel available for users of this distribution, at L on C|irc://irc.perl.org/#toolchain>. =head1 AUTHOR Original code from Module::Build::ModuleInfo by Ken Williams , Randy W. Sims Released as Module::Metadata by Matt S Trout (mst) with assistance from David Golden (xdg) . =head1 CONTRIBUTORS =for stopwords Karen Etheridge David Golden Vincent Pit Matt S Trout Chris Nehren Graham Knop Olivier Mengué Tomas Doran tokuhirom Christian Walde Tatsuhiko Miyagawa Peter Rabbitson Steve Hay Jerry D. Hedden Craig A. Berry Mitchell Steinbrunner Edward Zborowski Gareth Harper James Raspass 'BinGOs' Williams Josh Jore Kent Fredric =over 4 =item * Karen Etheridge =item * David Golden =item * Vincent Pit =item * Matt S Trout =item * Chris Nehren =item * Graham Knop =item * Olivier Mengué =item * Tomas Doran =item * tokuhirom =item * Christian Walde =item * Tatsuhiko Miyagawa =item * Peter Rabbitson =item * Steve Hay =item * Jerry D. Hedden =item * Craig A. Berry =item * Craig A. Berry =item * David Mitchell =item * David Steinbrunner =item * Edward Zborowski =item * Gareth Harper =item * James Raspass =item * Chris 'BinGOs' Williams =item * Josh Jore =item * Kent Fredric =back =head1 COPYRIGHT & LICENSE Original code Copyright (c) 2001-2011 Ken Williams. Additional code Copyright (c) 2010-2011 Matt Trout and David Golden. All rights reserved. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut MODULE_METADATA $fatpacked{"Parse/CPAN/Meta.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'PARSE_CPAN_META'; use 5.008001; use strict; package Parse::CPAN::Meta; # ABSTRACT: Parse META.yml and META.json CPAN metadata files our $VERSION = '1.4414'; # VERSION use Exporter; use Carp 'croak'; our @ISA = qw/Exporter/; our @EXPORT_OK = qw/Load LoadFile/; sub load_file { my ($class, $filename) = @_; my $meta = _slurp($filename); if ($filename =~ /\.ya?ml$/) { return $class->load_yaml_string($meta); } elsif ($filename =~ /\.json$/) { return $class->load_json_string($meta); } else { $class->load_string($meta); # try to detect yaml/json } } sub load_string { my ($class, $string) = @_; if ( $string =~ /^---/ ) { # looks like YAML return $class->load_yaml_string($string); } elsif ( $string =~ /^\s*\{/ ) { # looks like JSON return $class->load_json_string($string); } else { # maybe doc-marker-free YAML return $class->load_yaml_string($string); } } sub load_yaml_string { my ($class, $string) = @_; my $backend = $class->yaml_backend(); my $data = eval { no strict 'refs'; &{"$backend\::Load"}($string) }; croak $@ if $@; return $data || {}; # in case document was valid but empty } sub load_json_string { my ($class, $string) = @_; my $data = eval { $class->json_backend()->new->decode($string) }; croak $@ if $@; return $data || {}; } sub yaml_backend { if (! defined $ENV{PERL_YAML_BACKEND} ) { _can_load( 'CPAN::Meta::YAML', 0.011 ) or croak "CPAN::Meta::YAML 0.011 is not available\n"; return "CPAN::Meta::YAML"; } else { my $backend = $ENV{PERL_YAML_BACKEND}; _can_load( $backend ) or croak "Could not load PERL_YAML_BACKEND '$backend'\n"; $backend->can("Load") or croak "PERL_YAML_BACKEND '$backend' does not implement Load()\n"; return $backend; } } sub json_backend { if (! $ENV{PERL_JSON_BACKEND} or $ENV{PERL_JSON_BACKEND} eq 'JSON::PP') { _can_load( 'JSON::PP' => 2.27103 ) or croak "JSON::PP 2.27103 is not available\n"; return 'JSON::PP'; } else { _can_load( 'JSON' => 2.5 ) or croak "JSON 2.5 is required for " . "\$ENV{PERL_JSON_BACKEND} = '$ENV{PERL_JSON_BACKEND}'\n"; return "JSON"; } } sub _slurp { require Encode; open my $fh, "<:raw", "$_[0]" ## no critic or die "can't open $_[0] for reading: $!"; my $content = do { local $/; <$fh> }; $content = Encode::decode('UTF-8', $content, Encode::PERLQQ()); return $content; } sub _can_load { my ($module, $version) = @_; (my $file = $module) =~ s{::}{/}g; $file .= ".pm"; return 1 if $INC{$file}; return 0 if exists $INC{$file}; # prior load failed eval { require $file; 1 } or return 0; if ( defined $version ) { eval { $module->VERSION($version); 1 } or return 0; } return 1; } # Kept for backwards compatibility only # Create an object from a file sub LoadFile ($) { return Load(_slurp(shift)); } # Parse a document from a string. sub Load ($) { require CPAN::Meta::YAML; my $object = eval { CPAN::Meta::YAML::Load(shift) }; croak $@ if $@; return $object; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Parse::CPAN::Meta - Parse META.yml and META.json CPAN metadata files =head1 VERSION version 1.4414 =head1 SYNOPSIS ############################################# # In your file --- name: My-Distribution version: 1.23 resources: homepage: "http://example.com/dist/My-Distribution" ############################################# # In your program use Parse::CPAN::Meta; my $distmeta = Parse::CPAN::Meta->load_file('META.yml'); # Reading properties my $name = $distmeta->{name}; my $version = $distmeta->{version}; my $homepage = $distmeta->{resources}{homepage}; =head1 DESCRIPTION B is a parser for F and F files, using L and/or L. B provides three methods: C, C, and C. These will read and deserialize CPAN metafiles, and are described below in detail. B provides a legacy API of only two functions, based on the YAML functions of the same name. Wherever possible, identical calling semantics are used. These may only be used with YAML sources. All error reporting is done with exceptions (die'ing). Note that META files are expected to be in UTF-8 encoding, only. When converted string data, it must first be decoded from UTF-8. =begin Pod::Coverage =end Pod::Coverage =head1 METHODS =head2 load_file my $metadata_structure = Parse::CPAN::Meta->load_file('META.json'); my $metadata_structure = Parse::CPAN::Meta->load_file('META.yml'); This method will read the named file and deserialize it to a data structure, determining whether it should be JSON or YAML based on the filename. The file will be read using the ":utf8" IO layer. =head2 load_yaml_string my $metadata_structure = Parse::CPAN::Meta->load_yaml_string($yaml_string); This method deserializes the given string of YAML and returns the first document in it. (CPAN metadata files should always have only one document.) If the source was UTF-8 encoded, the string must be decoded before calling C. =head2 load_json_string my $metadata_structure = Parse::CPAN::Meta->load_json_string($json_string); This method deserializes the given string of JSON and the result. If the source was UTF-8 encoded, the string must be decoded before calling C. =head2 load_string my $metadata_structure = Parse::CPAN::Meta->load_string($some_string); If you don't know whether a string contains YAML or JSON data, this method will use some heuristics and guess. If it can't tell, it assumes YAML. =head2 yaml_backend my $backend = Parse::CPAN::Meta->yaml_backend; Returns the module name of the YAML serializer. See L for details. =head2 json_backend my $backend = Parse::CPAN::Meta->json_backend; Returns the module name of the JSON serializer. This will either be L or L. Even if C is set, this will return L as further delegation is handled by the L module. See L for details. =head1 FUNCTIONS For maintenance clarity, no functions are exported by default. These functions are available for backwards compatibility only and are best avoided in favor of C. =head2 Load my @yaml = Parse::CPAN::Meta::Load( $string ); Parses a string containing a valid YAML stream into a list of Perl data structures. =head2 LoadFile my @yaml = Parse::CPAN::Meta::LoadFile( 'META.yml' ); Reads the YAML stream from a file instead of a string. =head1 ENVIRONMENT =head2 PERL_JSON_BACKEND By default, L will be used for deserializing JSON data. If the C environment variable exists, is true and is not "JSON::PP", then the L module (version 2.5 or greater) will be loaded and used to interpret C. If L is not installed or is too old, an exception will be thrown. =head2 PERL_YAML_BACKEND By default, L will be used for deserializing YAML data. If the C environment variable is defined, then it is interpreted as a module to use for deserialization. The given module must be installed, must load correctly and must implement the C function or an exception will be thrown. =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/Perl-Toolchain-Gang/Parse-CPAN-Meta.git =head1 AUTHORS =over 4 =item * Adam Kennedy =item * David Golden =back =head1 CONTRIBUTORS =over 4 =item * Graham Knop =item * Joshua ben Jore =item * Neil Bowers =item * Ricardo Signes =item * Steffen Mueller =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2014 by Adam Kennedy and Contributors. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut PARSE_CPAN_META $fatpacked{"Parse/PMFile.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'PARSE_PMFILE'; package Parse::PMFile; sub __clean_eval { eval $_[0] } # needs to be here (RT#101273) use strict; use warnings; use Safe; use JSON::PP (); use Dumpvalue; use version (); use File::Spec (); our $VERSION = '0.41'; our $VERBOSE = 0; our $ALLOW_DEV_VERSION = 0; our $FORK = 0; our $UNSAFE = $] < 5.010000 ? 1 : 0; sub new { my ($class, $meta, $opts) = @_; bless {%{ $opts || {} }, META_CONTENT => $meta}, $class; } # from PAUSE::pmfile::examine_fio sub parse { my ($self, $pmfile) = @_; $pmfile =~ s|\\|/|g; my($filemtime) = (stat $pmfile)[9]; $self->{MTIME} = $filemtime; $self->{PMFILE} = $pmfile; unless ($self->_version_from_meta_ok) { my $version; unless (eval { $version = $self->_parse_version; 1 }) { $self->_verbose(1, "error with version in $pmfile: $@"); return; } $self->{VERSION} = $version; if ($self->{VERSION} =~ /^\{.*\}$/) { # JSON error message } elsif ($self->{VERSION} =~ /[_\s]/ && !$self->{ALLOW_DEV_VERSION} && !$ALLOW_DEV_VERSION){ # ignore developer releases and "You suck!" return; } } my($ppp) = $self->_packages_per_pmfile; my @keys_ppp = $self->_filter_ppps(sort keys %$ppp); $self->_verbose(1,"Will check keys_ppp[@keys_ppp]\n"); # # Immediately after each package (pmfile) examined contact # the database # my ($package, %errors); my %checked_in; DBPACK: foreach $package (@keys_ppp) { # this part is taken from PAUSE::package::examine_pkg # and PAUSE::package::_pkg_name_insane if ($package !~ /^\w[\w\:\']*\w?\z/ || $package !~ /\w\z/ || $package =~ /:/ && $package !~ /::/ || $package =~ /\w:\w/ || $package =~ /:::/ ){ $self->_verbose(1,"Package[$package] did not pass the ultimate sanity check"); delete $ppp->{$package}; next; } if ($self->{USERID} && $self->{PERMISSIONS} && !$self->_perm_check($package)) { delete $ppp->{$package}; next; } # Check that package name matches case of file name { my (undef, $module) = split m{/lib/}, $self->{PMFILE}, 2; if ($module) { $module =~ s{\.pm\z}{}; $module =~ s{/}{::}g; if (lc $module eq lc $package && $module ne $package) { # warn "/// $self->{PMFILE} vs. $module vs. $package\n"; $errors{$package} = { indexing_warning => "Capitalization of package ($package) does not match filename!", infile => $self->{PMFILE}, }; } } } my $pp = $ppp->{$package}; if ($pp->{version} && $pp->{version} =~ /^\{.*\}$/) { # JSON parser error my $err = JSON::PP::decode_json($pp->{version}); if ($err->{x_normalize}) { $errors{$package} = { normalize => $err->{version}, infile => $pp->{infile}, }; $pp->{version} = "undef"; } elsif ($err->{openerr}) { $pp->{version} = "undef"; $self->_verbose(1, qq{Parse::PMFile was not able to read the file. It issued the following error: C< $err->{r} >}, ); $errors{$package} = { open => $err->{r}, infile => $pp->{infile}, }; } else { $pp->{version} = "undef"; $self->_verbose(1, qq{Parse::PMFile was not able to parse the following line in that file: C< $err->{line} > Note: the indexer is running in a Safe compartement and cannot provide the full functionality of perl in the VERSION line. It is trying hard, but sometime it fails. As a workaround, please consider writing a META.yml that contains a 'provides' attribute or contact the CPAN admins to investigate (yet another) workaround against "Safe" limitations.)}, ); $errors{$package} = { parse_version => $err->{line}, infile => $err->{file}, }; } } # Sanity checks for ( $package, $pp->{version}, ) { if (!defined || /^\s*$/ || /\s/){ # for whatever reason I come here delete $ppp->{$package}; next; # don't screw up 02packages } } unless ($self->_version_ok($pp)) { $errors{$package} = { long_version => qq{Version string exceeds maximum allowed length of 16b: "$pp->{version}"}, infile => $pp->{infile}, }; next; } $checked_in{$package} = $ppp->{$package}; } # end foreach package return (wantarray && %errors) ? (\%checked_in, \%errors) : \%checked_in; } sub _version_ok { my ($self, $pp) = @_; return if length($pp->{version} || 0) > 16; return 1 } sub _perm_check { my ($self, $package) = @_; my $userid = $self->{USERID}; my $module = $self->{PERMISSIONS}->module_permissions($package); return 1 if !$module; # not listed yet return 1 if defined $module->m && $module->m eq $userid; return 1 if defined $module->f && $module->f eq $userid; return 1 if defined $module->c && grep {$_ eq $userid} @{$module->c}; return; } # from PAUSE::pmfile; sub _parse_version { my $self = shift; use strict; my $pmfile = $self->{PMFILE}; my $tmpfile = File::Spec->catfile(File::Spec->tmpdir, "ParsePMFile$$" . rand(1000)); my $pmcp = $pmfile; for ($pmcp) { s/([^\\](\\\\)*)@/$1\\@/g; # thanks to Raphael Manfredi for the # solution to escape @s and \ } my($v); { package main; # seems necessary # XXX: do we need to fork as PAUSE does? # or, is alarm() just fine? my $pid; if ($self->{FORK} || $FORK) { $pid = fork(); die "Can't fork: $!" unless defined $pid; } if ($pid) { waitpid($pid, 0); if (open my $fh, '<', $tmpfile) { $v = <$fh>; } } else { # XXX Limit Resources too my($comp) = Safe->new; my $eval = qq{ local(\$^W) = 0; Parse::PMFile::_parse_version_safely("$pmcp"); }; $comp->permit("entereval"); # for MBARBON/Module-Info-0.30.tar.gz $comp->share("*Parse::PMFile::_parse_version_safely"); $comp->share("*version::new"); $comp->share("*version::numify"); $comp->share_from('main', ['*version::', '*charstar::', '*Exporter::', '*DynaLoader::']); $comp->share_from('version', ['&qv']); $comp->permit(":base_math"); # atan2 (Acme-Pi) # $comp->permit("require"); # no strict! $comp->deny(qw/enteriter iter unstack goto/); # minimum protection against Acme::BadExample version->import('qv') if $self->{UNSAFE} || $UNSAFE; { no strict; $v = ($self->{UNSAFE} || $UNSAFE) ? eval $eval : $comp->reval($eval); } if ($@){ # still in the child process, out of Safe::reval my $err = $@; # warn ">>>>>>>err[$err]<<<<<<<<"; if (ref $err) { if ($err->{line} =~ /([\$*])([\w\:\']*)\bVERSION\b.*?\=(.*)/) { local($^W) = 0; my ($sigil, $vstr) = ($1, $3); $self->_restore_overloaded_stuff(1) if $err->{line} =~ /use\s+version\b|version\->|qv\(/; $v = ($self->{UNSAFE} || $UNSAFE) ? eval $vstr : $comp->reval($vstr); $v = $$v if $sigil eq '*' && ref $v; } if ($@ or !$v) { $self->_verbose(1, sprintf("reval failed: err[%s] for eval[%s]", JSON::PP::encode_json($err), $eval, )); $v = JSON::PP::encode_json($err); } } else { $v = JSON::PP::encode_json({ openerr => $err }); } } if (defined $v) { no warnings; $v = $v->numify if ref($v) =~ /^version(::vpp)?$/; } else { $v = ""; } if ($self->{FORK} || $FORK) { open my $fh, '>:utf8', $tmpfile; print $fh $v; exit 0; } else { utf8::encode($v); # undefine empty $v as if read from the tmpfile $v = undef if defined $v && !length $v; $comp->erase; $self->_restore_overloaded_stuff; } } } unlink $tmpfile if ($self->{FORK} || $FORK) && -e $tmpfile; return $self->_normalize_version($v); } sub _restore_overloaded_stuff { my ($self, $used_version_in_safe) = @_; return if $self->{UNSAFE} || $UNSAFE; no strict 'refs'; no warnings 'redefine'; # version XS in CPAN my $restored; if ($INC{'version/vxs.pm'}) { *{'version::(""'} = \&version::vxs::stringify; *{'version::(0+'} = \&version::vxs::numify; *{'version::(cmp'} = \&version::vxs::VCMP; *{'version::(<=>'} = \&version::vxs::VCMP; *{'version::(bool'} = \&version::vxs::boolean; $restored = 1; } # version PP in CPAN if ($INC{'version/vpp.pm'}) { { package # hide from PAUSE charstar; overload->import; } if (!$used_version_in_safe) { package # hide from PAUSE version::vpp; overload->import; } unless ($restored) { *{'version::(""'} = \&version::vpp::stringify; *{'version::(0+'} = \&version::vpp::numify; *{'version::(cmp'} = \&version::vpp::vcmp; *{'version::(<=>'} = \&version::vpp::vcmp; *{'version::(bool'} = \&version::vpp::vbool; } *{'version::vpp::(""'} = \&version::vpp::stringify; *{'version::vpp::(0+'} = \&version::vpp::numify; *{'version::vpp::(cmp'} = \&version::vpp::vcmp; *{'version::vpp::(<=>'} = \&version::vpp::vcmp; *{'version::vpp::(bool'} = \&version::vpp::vbool; *{'charstar::(""'} = \&charstar::thischar; *{'charstar::(0+'} = \&charstar::thischar; *{'charstar::(++'} = \&charstar::increment; *{'charstar::(--'} = \&charstar::decrement; *{'charstar::(+'} = \&charstar::plus; *{'charstar::(-'} = \&charstar::minus; *{'charstar::(*'} = \&charstar::multiply; *{'charstar::(cmp'} = \&charstar::cmp; *{'charstar::(<=>'} = \&charstar::spaceship; *{'charstar::(bool'} = \&charstar::thischar; *{'charstar::(='} = \&charstar::clone; $restored = 1; } # version in core if (!$restored) { *{'version::(""'} = \&version::stringify; *{'version::(0+'} = \&version::numify; *{'version::(cmp'} = \&version::vcmp; *{'version::(<=>'} = \&version::vcmp; *{'version::(bool'} = \&version::boolean; } } # from PAUSE::pmfile; sub _packages_per_pmfile { my $self = shift; my $ppp = {}; my $pmfile = $self->{PMFILE}; my $filemtime = $self->{MTIME}; my $version = $self->{VERSION}; open my $fh, "<", "$pmfile" or return $ppp; local $/ = "\n"; my $inpod = 0; PLINE: while (<$fh>) { chomp; my($pline) = $_; $inpod = $pline =~ /^=(?!cut)/ ? 1 : $pline =~ /^=cut/ ? 0 : $inpod; next if $inpod; next if substr($pline,0,4) eq "=cut"; $pline =~ s/\#.*//; next if $pline =~ /^\s*$/; if ($pline =~ /^__(?:END|DATA)__\b/ and $pmfile !~ /\.PL$/ # PL files may well have code after __DATA__ ){ last PLINE; } my $pkg; my $strict_version; if ( $pline =~ m{ # (.*) # takes too much time if $pline is long #(? 128; #restriction $ppp->{$pkg}{parsed}++; $ppp->{$pkg}{infile} = $pmfile; if ($self->_simile($pmfile,$pkg)) { $ppp->{$pkg}{simile} = $pmfile; if ($self->_version_from_meta_ok) { my $provides = $self->{META_CONTENT}{provides}; if (exists $provides->{$pkg}) { if (defined $provides->{$pkg}{version}) { my $v = $provides->{$pkg}{version}; if ($v =~ /[_\s]/ && !$self->{ALLOW_DEV_VERSION} && !$ALLOW_DEV_VERSION){ # ignore developer releases and "You suck!" next PLINE; } unless (eval { $version = $self->_normalize_version($v); 1 }) { $self->_verbose(1, "error with version in $pmfile: $@"); next; } $ppp->{$pkg}{version} = $version; } else { $ppp->{$pkg}{version} = "undef"; } } } else { if (defined $strict_version){ $ppp->{$pkg}{version} = $strict_version ; } else { $ppp->{$pkg}{version} = defined $version ? $version : ""; } no warnings; if ($version eq 'undef') { $ppp->{$pkg}{version} = $version unless defined $ppp->{$pkg}{version}; } else { $ppp->{$pkg}{version} = $version if $version > $ppp->{$pkg}{version} || $version gt $ppp->{$pkg}{version}; } } } else { # not simile #### it comes later, it would be nonsense #### to set to "undef". MM_Unix gives us #### the best we can reasonably consider $ppp->{$pkg}{version} = $version unless defined $ppp->{$pkg}{version} && length($ppp->{$pkg}{version}); } $ppp->{$pkg}{filemtime} = $filemtime; } else { # $self->_verbose(2,"no pkg found"); } } close $fh; $ppp; } # from PAUSE::pmfile; { no strict; sub _parse_version_safely { my($parsefile) = @_; my $result; local *FH; local $/ = "\n"; open(FH,$parsefile) or die "Could not open '$parsefile': $!"; my $inpod = 0; while () { $inpod = /^=(?!cut)/ ? 1 : /^=cut/ ? 0 : $inpod; next if $inpod || /^\s*#/; last if /^__(?:END|DATA)__\b/; # fails on quoted __END__ but this is rare -> __END__ in the middle of a line is rarer chop; if (my ($ver) = /package \s+ \S+ \s+ (\S+) \s* [;{]/x) { # XXX: should handle this better if version is bogus -- rjbs, # 2014-03-16 return $ver if version::is_lax($ver); } # next unless /\$(([\w\:\']*)\bVERSION)\b.*\=/; next unless /(?<=])\=(?![=>])/; my $current_parsed_line = $_; my $eval = qq{ package # ExtUtils::MakeMaker::_version; local $1$2; \$$2=undef; do { $_ }; \$$2 }; local $^W = 0; local $SIG{__WARN__} = sub {}; $result = __clean_eval($eval); # warn "current_parsed_line[$current_parsed_line]\$\@[$@]"; if ($@ or !defined $result){ die +{ eval => $eval, line => $current_parsed_line, file => $parsefile, err => $@, }; } last; } #; close FH; $result = "undef" unless defined $result; if ((ref $result) =~ /^version(?:::vpp)?\b/) { no warnings; $result = $result->numify; } return $result; } } # from PAUSE::pmfile; sub _filter_ppps { my($self,@ppps) = @_; my @res; # very similar code is in PAUSE::dist::filter_pms MANI: for my $ppp ( @ppps ) { if ($self->{META_CONTENT}){ my $no_index = $self->{META_CONTENT}{no_index} || $self->{META_CONTENT}{private}; # backward compat if (ref($no_index) eq 'HASH') { my %map = ( package => qr{\z}, namespace => qr{::}, ); for my $k (qw(package namespace)) { next unless my $v = $no_index->{$k}; my $rest = $map{$k}; if (ref $v eq "ARRAY") { for my $ve (@$v) { $ve =~ s|::$||; if ($ppp =~ /^$ve$rest/){ $self->_verbose(1,"Skipping ppp[$ppp] due to ve[$ve]"); next MANI; } else { $self->_verbose(1,"NOT skipping ppp[$ppp] due to ve[$ve]"); } } } else { $v =~ s|::$||; if ($ppp =~ /^$v$rest/){ $self->_verbose(1,"Skipping ppp[$ppp] due to v[$v]"); next MANI; } else { $self->_verbose(1,"NOT skipping ppp[$ppp] due to v[$v]"); } } } } else { $self->_verbose(1,"No keyword 'no_index' or 'private' in META_CONTENT"); } } else { # $self->_verbose(1,"no META_CONTENT"); # too noisy } push @res, $ppp; } $self->_verbose(1,"Result of filter_ppps: res[@res]"); @res; } # from PAUSE::pmfile; sub _simile { my($self,$file,$package) = @_; # MakeMaker gives them the chance to have the file Simple.pm in # this directory but have the package HTML::Simple in it. # Afaik, they wouldn't be able to do so with deeper nested packages $file =~ s|.*/||; $file =~ s|\.pm(?:\.PL)?||; my $ret = $package =~ m/\b\Q$file\E$/; $ret ||= 0; unless ($ret) { # Apache::mod_perl_guide stuffs it into Version.pm $ret = 1 if lc $file eq 'version'; } $self->_verbose(1,"Result of simile(): file[$file] package[$package] ret[$ret]\n"); $ret; } # from PAUSE::pmfile sub _normalize_version { my($self,$v) = @_; $v = "undef" unless defined $v; my $dv = Dumpvalue->new; my $sdv = $dv->stringify($v,1); # second argument prevents ticks $self->_verbose(1,"Result of normalize_version: sdv[$sdv]\n"); return $v if $v eq "undef"; return $v if $v =~ /^\{.*\}$/; # JSON object $v =~ s/^\s+//; $v =~ s/\s+\z//; if ($v =~ /_/) { # XXX should pass something like EDEVELOPERRELEASE up e.g. # SIXTEASE/XML-Entities-0.0306.tar.gz had nothing but one # such modules and the mesage was not helpful that "nothing # was found". return $v ; } if (!version::is_lax($v)) { return JSON::PP::encode_json({ x_normalize => 'version::is_lax failed', version => $v }); } # may warn "Integer overflow" my $vv = eval { no warnings; version->new($v)->numify }; if ($@) { # warn "$v: $@"; return JSON::PP::encode_json({ x_normalize => $@, version => $v }); # return "undef"; } if ($vv eq $v) { # the boring 3.14 } else { my $forced = $self->_force_numeric($v); if ($forced eq $vv) { } elsif ($forced =~ /^v(.+)/) { # rare case where a v1.0.23 slipped in (JANL/w3mir-1.0.10.tar.gz) no warnings; $vv = version->new($1)->numify; } else { # warn "Unequal forced[$forced] and vv[$vv]"; if ($forced == $vv) { # the trailing zeroes would cause unnecessary havoc $vv = $forced; } } } return $vv; } # from PAUSE::pmfile; sub _force_numeric { my($self,$v) = @_; $v = $self->_readable($v); if ( $v =~ /^(\+?)(\d*)(\.(\d*))?/ && # "$2$4" ne '' ( defined $2 && length $2 || defined $4 && length $4 ) ) { my $two = defined $2 ? $2 : ""; my $three = defined $3 ? $3 : ""; $v = "$two$three"; } # no else branch! We simply say, everything else is a string. $v; } # from PAUSE::dist sub _version_from_meta_ok { my($self) = @_; return $self->{VERSION_FROM_META_OK} if exists $self->{VERSION_FROM_META_OK}; my $c = $self->{META_CONTENT}; # If there's no provides hash, we can't get our module versions from the # provides hash! -- rjbs, 2012-03-31 return($self->{VERSION_FROM_META_OK} = 0) unless $c->{provides}; # Some versions of Module::Build geneated an empty provides hash. If we're # *not* looking at a Module::Build-generated metafile, then it's okay. my ($mb_v) = (defined $c->{generated_by} ? $c->{generated_by} : '') =~ /Module::Build version ([\d\.]+)/; return($self->{VERSION_FROM_META_OK} = 1) unless $mb_v; # ??? I don't know why this is here. return($self->{VERSION_FROM_META_OK} = 1) if $mb_v eq '0.250.0'; if ($mb_v >= 0.19 && $mb_v < 0.26 && ! keys %{$c->{provides}}) { # RSAVAGE/Javascript-SHA1-1.01.tgz had an empty provides hash. Ron # did not find the reason why this happened, but let's not go # overboard, 0.26 seems a good threshold from the statistics: there # are not many empty provides hashes from 0.26 up. return($self->{VERSION_FROM_META_OK} = 0); } # We're not in the suspect range of M::B versions. It's good to go. return($self->{VERSION_FROM_META_OK} = 1); } sub _verbose { my($self,$level,@what) = @_; warn @what if $level <= ((ref $self && $self->{VERBOSE}) || $VERBOSE); } # all of the following methods are stripped from CPAN::Version # (as of version 5.5001, bundled in CPAN 2.03), and slightly # modified (ie. made private, as well as CPAN->debug(...) are # replaced with $self->_verbose(9, ...).) # CPAN::Version::vcmp courtesy Jost Krieger sub _vcmp { my($self,$l,$r) = @_; local($^W) = 0; $self->_verbose(9, "l[$l] r[$r]"); return 0 if $l eq $r; # short circuit for quicker success for ($l,$r) { s/_//g; } $self->_verbose(9, "l[$l] r[$r]"); for ($l,$r) { next unless tr/.// > 1 || /^v/; s/^v?/v/; 1 while s/\.0+(\d)/.$1/; # remove leading zeroes per group } $self->_verbose(9, "l[$l] r[$r]"); if ($l=~/^v/ <=> $r=~/^v/) { for ($l,$r) { next if /^v/; $_ = $self->_float2vv($_); } } $self->_verbose(9, "l[$l] r[$r]"); my $lvstring = "v0"; my $rvstring = "v0"; if ($] >= 5.006 && $l =~ /^v/ && $r =~ /^v/) { $lvstring = $self->_vstring($l); $rvstring = $self->_vstring($r); $self->_verbose(9, sprintf "lv[%vd] rv[%vd]", $lvstring, $rvstring); } return ( ($l ne "undef") <=> ($r ne "undef") || $lvstring cmp $rvstring || $l <=> $r || $l cmp $r ); } sub _vgt { my($self,$l,$r) = @_; $self->_vcmp($l,$r) > 0; } sub _vlt { my($self,$l,$r) = @_; $self->_vcmp($l,$r) < 0; } sub _vge { my($self,$l,$r) = @_; $self->_vcmp($l,$r) >= 0; } sub _vle { my($self,$l,$r) = @_; $self->_vcmp($l,$r) <= 0; } sub _vstring { my($self,$n) = @_; $n =~ s/^v// or die "Parse::PMFile::_vstring() called with invalid arg [$n]"; pack "U*", split /\./, $n; } # vv => visible vstring sub _float2vv { my($self,$n) = @_; my($rev) = int($n); $rev ||= 0; my($mantissa) = $n =~ /\.(\d{1,12})/; # limit to 12 digits to limit # architecture influence $mantissa ||= 0; $mantissa .= "0" while length($mantissa)%3; my $ret = "v" . $rev; while ($mantissa) { $mantissa =~ s/(\d{1,3})// or die "Panic: length>0 but not a digit? mantissa[$mantissa]"; $ret .= ".".int($1); } # warn "n[$n]ret[$ret]"; $ret =~ s/(\.0)+/.0/; # v1.0.0 => v1.0 $ret; } sub _readable { my($self,$n) = @_; $n =~ /^([\w\-\+\.]+)/; return $1 if defined $1 && length($1)>0; # if the first user reaches version v43, he will be treated as "+". # We'll have to decide about a new rule here then, depending on what # will be the prevailing versioning behavior then. if ($] < 5.006) { # or whenever v-strings were introduced # we get them wrong anyway, whatever we do, because 5.005 will # have already interpreted 0.2.4 to be "0.24". So even if he # indexer sends us something like "v0.2.4" we compare wrongly. # And if they say v1.2, then the old perl takes it as "v12" $self->_verbose(9, "Suspicious version string seen [$n]\n"); return $n; } my $better = sprintf "v%vd", $n; $self->_verbose(9, "n[$n] better[$better]"); return $better; } 1; __END__ =head1 NAME Parse::PMFile - parses .pm file as PAUSE does =head1 SYNOPSIS use Parse::PMFile; my $parser = Parse::PMFile->new($metadata, {VERBOSE => 1}); my $packages_info = $parser->parse($pmfile); # if you need info about invalid versions my ($packages_info, $errors) = $parser->parse($pmfile); # to check permissions my $parser = Parse::PMFile->new($metadata, { USERID => 'ISHIGAKI', PERMISSIONS => PAUSE::Permissions->new, }); =head1 DESCRIPTION The most of the code of this module is taken from the PAUSE code as of April 2013 almost verbatim. Thus, the heart of this module should be quite stable. However, I made it not to use pipe ("-|") as well as I stripped database-related code. If you encounter any issue, that's most probably because of my modification. This module doesn't provide features to extract a distribution or parse meta files intentionally. =head1 METHODS =head2 new creates an object. You can also pass a hashref taken from META.yml etc, and an optional hashref. Options are: =over 4 =item ALLOW_DEV_VERSION Parse::PMFile usually ignores a version with an underscore as PAUSE does (because it's for a developer release, and should not be indexed). Set this option to true if you happen to need to keep such a version for better analysis. =item VERBOSE Set this to true if you need to know some details. =item FORK As of version 0.17, Parse::PMFile stops forking while parsing a version for better performance. Parse::PMFile should return the same result no matter how this option is set, but if you do care, set this to true to fork as PAUSE does. =item USERID, PERMISSIONS As of version 0.21, Parse::PMFile checks permissions of a package if both USERID and PERMISSIONS (which should be an instance of L) are provided. Unauthorized packages are removed. =item UNSAFE Parse::PMFile usually parses a module version in a Safe compartment. However, this approach doesn't work smoothly under older perls (prior to 5.10) plus some combinations of recent versions of Safe.pm (2.24 and above) and version.pm (0.9905 and above) for various reasons. As of version 0.27, Parse::PMFile simply uses C to parse a version under older perls. If you want it to use always C (even under recent perls), set this to true. =back =head2 parse takes a path to a .pm file, and returns a hash reference that holds information for package(s) found in the file. =head1 SEE ALSO L, L Most part of this module is derived from PAUSE and CPAN::Version. L L =head1 AUTHOR Andreas Koenig Eandreas.koenig@anima.deE Kenichi Ishigaki, Eishigaki@cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 1995 - 2013 by Andreas Koenig Eandk@cpan.orgE for most of the code. Copyright 2013 by Kenichi Ishigaki for some. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut PARSE_PMFILE $fatpacked{"Path/Tiny.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'PATH_TINY'; use 5.008001; use strict; use warnings; package Path::Tiny; # ABSTRACT: File path utility our $VERSION = '0.108'; # Dependencies use Config; use Exporter 5.57 (qw/import/); use File::Spec 0.86 (); # shipped with 5.8.1 use Carp (); our @EXPORT = qw/path/; our @EXPORT_OK = qw/cwd rootdir tempfile tempdir/; use constant { PATH => 0, CANON => 1, VOL => 2, DIR => 3, FILE => 4, TEMP => 5, IS_WIN32 => ( $^O eq 'MSWin32' ), }; use overload ( q{""} => sub { $_[0]->[PATH] }, bool => sub () { 1 }, fallback => 1, ); # FREEZE/THAW per Sereal/CBOR/Types::Serialiser protocol sub FREEZE { return $_[0]->[PATH] } sub THAW { return path( $_[2] ) } { no warnings 'once'; *TO_JSON = *FREEZE }; my $HAS_UU; # has Unicode::UTF8; lazily populated sub _check_UU { local $SIG{__DIE__}; # prevent outer handler from being called !!eval { require Unicode::UTF8; Unicode::UTF8->VERSION(0.58); 1; }; } my $HAS_PU; # has PerlIO::utf8_strict; lazily populated sub _check_PU { local $SIG{__DIE__}; # prevent outer handler from being called !!eval { # MUST preload Encode or $SIG{__DIE__} localization fails # on some Perl 5.8.8 (maybe other 5.8.*) compiled with -O2. require Encode; require PerlIO::utf8_strict; PerlIO::utf8_strict->VERSION(0.003); 1; }; } my $HAS_FLOCK = $Config{d_flock} || $Config{d_fcntl_can_lock} || $Config{d_lockf}; # notions of "root" directories differ on Win32: \\server\dir\ or C:\ or \ my $SLASH = qr{[\\/]}; my $NOTSLASH = qr{[^\\/]}; my $DRV_VOL = qr{[a-z]:}i; my $UNC_VOL = qr{$SLASH $SLASH $NOTSLASH+ $SLASH $NOTSLASH+}x; my $WIN32_ROOT = qr{(?: $UNC_VOL $SLASH | $DRV_VOL $SLASH | $SLASH )}x; sub _win32_vol { my ( $path, $drv ) = @_; require Cwd; my $dcwd = eval { Cwd::getdcwd($drv) }; # C: -> C:\some\cwd # getdcwd on non-existent drive returns empty string # so just use the original drive Z: -> Z: $dcwd = "$drv" unless defined $dcwd && length $dcwd; # normalize dwcd to end with a slash: might be C:\some\cwd or D:\ or Z: $dcwd =~ s{$SLASH?$}{/}; # make the path absolute with dcwd $path =~ s{^$DRV_VOL}{$dcwd}; return $path; } # This is a string test for before we have the object; see is_rootdir for well-formed # object test sub _is_root { return IS_WIN32() ? ( $_[0] =~ /^$WIN32_ROOT$/ ) : ( $_[0] eq '/' ); } BEGIN { *_same = IS_WIN32() ? sub { lc( $_[0] ) eq lc( $_[1] ) } : sub { $_[0] eq $_[1] }; } # mode bits encoded for chmod in symbolic mode my %MODEBITS = ( om => 0007, gm => 0070, um => 0700 ); ## no critic { my $m = 0; $MODEBITS{$_} = ( 1 << $m++ ) for qw/ox ow or gx gw gr ux uw ur/ }; sub _symbolic_chmod { my ( $mode, $symbolic ) = @_; for my $clause ( split /,\s*/, $symbolic ) { if ( $clause =~ m{\A([augo]+)([=+-])([rwx]+)\z} ) { my ( $who, $action, $perms ) = ( $1, $2, $3 ); $who =~ s/a/ugo/g; for my $w ( split //, $who ) { my $p = 0; $p |= $MODEBITS{"$w$_"} for split //, $perms; if ( $action eq '=' ) { $mode = ( $mode & ~$MODEBITS{"${w}m"} ) | $p; } else { $mode = $action eq "+" ? ( $mode | $p ) : ( $mode & ~$p ); } } } else { Carp::croak("Invalid mode clause '$clause' for chmod()"); } } return $mode; } # flock doesn't work on NFS on BSD or on some filesystems like lustre. # Since program authors often can't control or detect that, we warn once # instead of being fatal if we can detect it and people who need it strict # can fatalize the 'flock' category #<<< No perltidy { package flock; use warnings::register } #>>> my $WARNED_NO_FLOCK = 0; sub _throw { my ( $self, $function, $file, $msg ) = @_; if ( $function =~ /^flock/ && $! =~ /operation not supported|function not implemented/i && !warnings::fatal_enabled('flock') ) { if ( !$WARNED_NO_FLOCK ) { warnings::warn( flock => "Flock not available: '$!': continuing in unsafe mode" ); $WARNED_NO_FLOCK++; } } else { $msg = $! unless defined $msg; Path::Tiny::Error->throw( $function, ( defined $file ? $file : $self->[PATH] ), $msg ); } return; } # cheapo option validation sub _get_args { my ( $raw, @valid ) = @_; if ( defined($raw) && ref($raw) ne 'HASH' ) { my ( undef, undef, undef, $called_as ) = caller(1); $called_as =~ s{^.*::}{}; Carp::croak("Options for $called_as must be a hash reference"); } my $cooked = {}; for my $k (@valid) { $cooked->{$k} = delete $raw->{$k} if exists $raw->{$k}; } if ( keys %$raw ) { my ( undef, undef, undef, $called_as ) = caller(1); $called_as =~ s{^.*::}{}; Carp::croak( "Invalid option(s) for $called_as: " . join( ", ", keys %$raw ) ); } return $cooked; } #--------------------------------------------------------------------------# # Constructors #--------------------------------------------------------------------------# #pod =construct path #pod #pod $path = path("foo/bar"); #pod $path = path("/tmp", "file.txt"); # list #pod $path = path("."); # cwd #pod $path = path("~user/file.txt"); # tilde processing #pod #pod Constructs a C object. It doesn't matter if you give a file or #pod directory path. It's still up to you to call directory-like methods only on #pod directories and file-like methods only on files. This function is exported #pod automatically by default. #pod #pod The first argument must be defined and have non-zero length or an exception #pod will be thrown. This prevents subtle, dangerous errors with code like #pod C<< path( maybe_undef() )->remove_tree >>. #pod #pod If the first component of the path is a tilde ('~') then the component will be #pod replaced with the output of C. If the first component of the path #pod is a tilde followed by a user name then the component will be replaced with #pod output of C. Behaviour for non-existent users depends on #pod the output of C on the system. #pod #pod On Windows, if the path consists of a drive identifier without a path component #pod (C or C), it will be expanded to the absolute path of the current #pod directory on that volume using C. #pod #pod If called with a single C argument, the original is returned unless #pod the original is holding a temporary file or directory reference in which case a #pod stringified copy is made. #pod #pod $path = path("foo/bar"); #pod $temp = Path::Tiny->tempfile; #pod #pod $p2 = path($path); # like $p2 = $path #pod $t2 = path($temp); # like $t2 = path( "$temp" ) #pod #pod This optimizes copies without proliferating references unexpectedly if a copy is #pod made by code outside your control. #pod #pod Current API available since 0.017. #pod #pod =cut sub path { my $path = shift; Carp::croak("Path::Tiny paths require defined, positive-length parts") unless 1 + @_ == grep { defined && length } $path, @_; # non-temp Path::Tiny objects are effectively immutable and can be reused if ( !@_ && ref($path) eq __PACKAGE__ && !$path->[TEMP] ) { return $path; } # stringify objects $path = "$path"; # expand relative volume paths on windows; put trailing slash on UNC root if ( IS_WIN32() ) { $path = _win32_vol( $path, $1 ) if $path =~ m{^($DRV_VOL)(?:$NOTSLASH|$)}; $path .= "/" if $path =~ m{^$UNC_VOL$}; } # concatenations stringifies objects, too if (@_) { $path .= ( _is_root($path) ? "" : "/" ) . join( "/", @_ ); } # canonicalize, but with unix slashes and put back trailing volume slash my $cpath = $path = File::Spec->canonpath($path); $path =~ tr[\\][/] if IS_WIN32(); $path = "/" if $path eq '/..'; # for old File::Spec $path .= "/" if IS_WIN32() && $path =~ m{^$UNC_VOL$}; # root paths must always have a trailing slash, but other paths must not if ( _is_root($path) ) { $path =~ s{/?$}{/}; } else { $path =~ s{/$}{}; } # do any tilde expansions if ( $path =~ m{^(~[^/]*).*} ) { require File::Glob; my ($homedir) = File::Glob::bsd_glob($1); $homedir =~ tr[\\][/] if IS_WIN32(); $path =~ s{^(~[^/]*)}{$homedir}; } bless [ $path, $cpath ], __PACKAGE__; } #pod =construct new #pod #pod $path = Path::Tiny->new("foo/bar"); #pod #pod This is just like C, but with method call overhead. (Why would you #pod do that?) #pod #pod Current API available since 0.001. #pod #pod =cut sub new { shift; path(@_) } #pod =construct cwd #pod #pod $path = Path::Tiny->cwd; # path( Cwd::getcwd ) #pod $path = cwd; # optional export #pod #pod Gives you the absolute path to the current directory as a C object. #pod This is slightly faster than C<< path(".")->absolute >>. #pod #pod C may be exported on request and used as a function instead of as a #pod method. #pod #pod Current API available since 0.018. #pod #pod =cut sub cwd { require Cwd; return path( Cwd::getcwd() ); } #pod =construct rootdir #pod #pod $path = Path::Tiny->rootdir; # / #pod $path = rootdir; # optional export #pod #pod Gives you C<< File::Spec->rootdir >> as a C object if you're too #pod picky for C. #pod #pod C may be exported on request and used as a function instead of as a #pod method. #pod #pod Current API available since 0.018. #pod #pod =cut sub rootdir { path( File::Spec->rootdir ) } #pod =construct tempfile, tempdir #pod #pod $temp = Path::Tiny->tempfile( @options ); #pod $temp = Path::Tiny->tempdir( @options ); #pod $temp = tempfile( @options ); # optional export #pod $temp = tempdir( @options ); # optional export #pod #pod C passes the options to C<< File::Temp->new >> and returns a C #pod object with the file name. The C option is enabled by default. #pod #pod The resulting C object is cached. When the C object is #pod destroyed, the C object will be as well. #pod #pod C annoyingly requires you to specify a custom template in slightly #pod different ways depending on which function or method you call, but #pod C lets you ignore that and can take either a leading template or a #pod C