1001 lines
30 KiB
Plaintext
1001 lines
30 KiB
Plaintext
|
#!/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{"MetaCPAN/API/Tiny.pm"} = <<'METACPAN_API_TINY';
|
||
|
package MetaCPAN::API::Tiny;
|
||
|
{
|
||
|
$MetaCPAN::API::Tiny::VERSION = '1.131730';
|
||
|
}
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
# ABSTRACT: A Tiny API client for MetaCPAN
|
||
|
|
||
|
use Carp;
|
||
|
use JSON::PP 'encode_json', 'decode_json';
|
||
|
use HTTP::Tiny;
|
||
|
|
||
|
|
||
|
sub new {
|
||
|
my ($class, @args) = @_;
|
||
|
|
||
|
$#_ % 2 == 0
|
||
|
or croak 'Arguments must be provided as name/value pairs';
|
||
|
|
||
|
my %params = @args;
|
||
|
|
||
|
die 'ua_args must be an array reference'
|
||
|
if $params{ua_args} && ref($params{ua_args}) ne 'ARRAY';
|
||
|
|
||
|
my $self = +{
|
||
|
base_url => $params{base_url} || 'https://api.metacpan.org/v0',
|
||
|
ua => $params{ua} || HTTP::Tiny->new(
|
||
|
$params{ua_args}
|
||
|
? @{$params{ua_args}}
|
||
|
: (agent => 'MetaCPAN::API::Tiny/'
|
||
|
. ($MetaCPAN::API::VERSION || 'xx'))),
|
||
|
};
|
||
|
|
||
|
return bless($self, $class);
|
||
|
}
|
||
|
|
||
|
sub _build_extra_params {
|
||
|
my $self = shift;
|
||
|
|
||
|
@_ % 2 == 0
|
||
|
or croak 'Incorrect number of params, must be key/value';
|
||
|
|
||
|
my %extra = @_;
|
||
|
my $ua = $self->{ua};
|
||
|
|
||
|
foreach my $key (keys %extra)
|
||
|
{
|
||
|
# The implementation in HTTP::Tiny uses + instead of %20, fix that
|
||
|
$extra{$key} = $ua->_uri_escape($extra{$key});
|
||
|
$extra{$key} =~ s/\+/%20/g;
|
||
|
}
|
||
|
|
||
|
my $params = join '&', map { "$_=" . $extra{$_} } sort keys %extra;
|
||
|
|
||
|
return $params;
|
||
|
}
|
||
|
|
||
|
|
||
|
# /source/{author}/{release}/{path}
|
||
|
sub source {
|
||
|
my $self = shift;
|
||
|
my %opts = @_ ? @_ : ();
|
||
|
my $url = '';
|
||
|
my $error = "Provide 'author' and 'release' and 'path'";
|
||
|
|
||
|
%opts or croak $error;
|
||
|
|
||
|
if (
|
||
|
defined ( my $author = $opts{'author'} ) &&
|
||
|
defined ( my $release = $opts{'release'} ) &&
|
||
|
defined ( my $path = $opts{'path'} )
|
||
|
) {
|
||
|
$url = "source/$author/$release/$path";
|
||
|
} else {
|
||
|
croak $error;
|
||
|
}
|
||
|
|
||
|
$url = $self->{base_url} . "/$url";
|
||
|
|
||
|
my $result = $self->{ua}->get($url);
|
||
|
$result->{'success'}
|
||
|
or croak "Failed to fetch '$url': " . $result->{'reason'};
|
||
|
|
||
|
return $result->{'content'};
|
||
|
}
|
||
|
|
||
|
|
||
|
# /release/{distribution}
|
||
|
# /release/{author}/{release}
|
||
|
sub release {
|
||
|
my $self = shift;
|
||
|
my %opts = @_ ? @_ : ();
|
||
|
my $url = '';
|
||
|
my $error = "Either provide 'distribution', or 'author' and 'release', " .
|
||
|
"or 'search'";
|
||
|
|
||
|
%opts or croak $error;
|
||
|
|
||
|
my %extra_opts = ();
|
||
|
|
||
|
if ( defined ( my $dist = $opts{'distribution'} ) ) {
|
||
|
$url = "release/$dist";
|
||
|
} elsif (
|
||
|
defined ( my $author = $opts{'author'} ) &&
|
||
|
defined ( my $release = $opts{'release'} )
|
||
|
) {
|
||
|
$url = "release/$author/$release";
|
||
|
} elsif ( defined ( my $search_opts = $opts{'search'} ) ) {
|
||
|
ref $search_opts && ref $search_opts eq 'HASH'
|
||
|
or croak $error;
|
||
|
|
||
|
%extra_opts = %{$search_opts};
|
||
|
$url = 'release/_search';
|
||
|
} else {
|
||
|
croak $error;
|
||
|
}
|
||
|
|
||
|
return $self->fetch( $url, %extra_opts );
|
||
|
}
|
||
|
|
||
|
|
||
|
# /pod/{module}
|
||
|
# /pod/{author}/{release}/{path}
|
||
|
sub pod {
|
||
|
my $self = shift;
|
||
|
my %opts = @_ ? @_ : ();
|
||
|
my $url = '';
|
||
|
my $error = "Either provide 'module' or 'author and 'release' and 'path'";
|
||
|
|
||
|
%opts or croak $error;
|
||
|
|
||
|
if ( defined ( my $module = $opts{'module'} ) ) {
|
||
|
$url = "pod/$module";
|
||
|
} elsif (
|
||
|
defined ( my $author = $opts{'author'} ) &&
|
||
|
defined ( my $release = $opts{'release'} ) &&
|
||
|
defined ( my $path = $opts{'path'} )
|
||
|
) {
|
||
|
$url = "pod/$author/$release/$path";
|
||
|
} else {
|
||
|
croak $error;
|
||
|
}
|
||
|
|
||
|
# check content-type
|
||
|
my %extra = ();
|
||
|
if ( defined ( my $type = $opts{'content-type'} ) ) {
|
||
|
$type =~ m{^ text/ (?: html|plain|x-pod|x-markdown ) $}x
|
||
|
or croak 'Incorrect content-type provided';
|
||
|
|
||
|
$extra{headers}{'content-type'} = $type;
|
||
|
}
|
||
|
|
||
|
$url = $self->{base_url}. "/$url";
|
||
|
|
||
|
my $result = $self->{ua}->get( $url, \%extra );
|
||
|
$result->{'success'}
|
||
|
or croak "Failed to fetch '$url': " . $result->{'reason'};
|
||
|
|
||
|
return $result->{'content'};
|
||
|
}
|
||
|
|
||
|
|
||
|
# /module/{module}
|
||
|
sub module {
|
||
|
my $self = shift;
|
||
|
my $name = shift;
|
||
|
|
||
|
$name or croak 'Please provide a module name';
|
||
|
|
||
|
return $self->fetch("module/$name");
|
||
|
}
|
||
|
|
||
|
|
||
|
# file() is a synonym of module
|
||
|
sub file { goto &module }
|
||
|
|
||
|
|
||
|
# /author/{author}
|
||
|
sub author {
|
||
|
my $self = shift;
|
||
|
my ( $pause_id, $url, %extra_opts );
|
||
|
|
||
|
if ( @_ == 1 ) {
|
||
|
$url = 'author/' . shift;
|
||
|
} elsif ( @_ == 2 ) {
|
||
|
my %opts = @_;
|
||
|
|
||
|
if ( defined $opts{'pauseid'} ) {
|
||
|
$url = "author/" . $opts{'pauseid'};
|
||
|
} elsif ( defined $opts{'search'} ) {
|
||
|
my $search_opts = $opts{'search'};
|
||
|
|
||
|
ref $search_opts && ref $search_opts eq 'HASH'
|
||
|
or croak "'search' key must be hashref";
|
||
|
|
||
|
%extra_opts = %{$search_opts};
|
||
|
$url = 'author/_search';
|
||
|
} else {
|
||
|
croak 'Unknown option given';
|
||
|
}
|
||
|
} else {
|
||
|
croak 'Please provide an author PAUSEID or a "search"';
|
||
|
}
|
||
|
|
||
|
return $self->fetch( $url, %extra_opts );
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
sub fetch {
|
||
|
my $self = shift;
|
||
|
my $url = shift;
|
||
|
my $extra = $self->_build_extra_params(@_);
|
||
|
my $base = $self->{base_url};
|
||
|
my $req_url = $extra ? "$base/$url?$extra" : "$base/$url";
|
||
|
|
||
|
my $result = $self->{ua}->get($req_url);
|
||
|
return $self->_decode_result( $result, $req_url );
|
||
|
}
|
||
|
|
||
|
|
||
|
sub post {
|
||
|
my $self = shift;
|
||
|
my $url = shift;
|
||
|
my $query = shift;
|
||
|
my $base = $self->{base_url};
|
||
|
|
||
|
defined $url
|
||
|
or croak 'First argument of URL must be provided';
|
||
|
|
||
|
ref $query and ref $query eq 'HASH'
|
||
|
or croak 'Second argument of query hashref must be provided';
|
||
|
|
||
|
my $query_json = encode_json( $query );
|
||
|
my $result = $self->{ua}->request(
|
||
|
'POST',
|
||
|
"$base/$url",
|
||
|
{
|
||
|
headers => { 'Content-Type' => 'application/json' },
|
||
|
content => $query_json,
|
||
|
}
|
||
|
);
|
||
|
|
||
|
return $self->_decode_result( $result, $url, $query_json );
|
||
|
}
|
||
|
|
||
|
sub _decode_result {
|
||
|
my $self = shift;
|
||
|
my ( $result, $url, $original ) = @_;
|
||
|
my $decoded_result;
|
||
|
|
||
|
ref $result and ref $result eq 'HASH'
|
||
|
or croak 'First argument must be hashref';
|
||
|
|
||
|
defined $url
|
||
|
or croak 'Second argument of a URL must be provided';
|
||
|
|
||
|
if ( defined ( my $success = $result->{'success'} ) ) {
|
||
|
my $reason = $result->{'reason'} || '';
|
||
|
$reason .= ( defined $original ? " (request: $original)" : '' );
|
||
|
|
||
|
$success or croak "Failed to fetch '$url': $reason";
|
||
|
} else {
|
||
|
croak 'Missing success in return value';
|
||
|
}
|
||
|
|
||
|
defined ( my $content = $result->{'content'} )
|
||
|
or croak 'Missing content in return value';
|
||
|
|
||
|
eval { $decoded_result = decode_json $content; 1 }
|
||
|
or do { croak "Couldn't decode '$content': $@" };
|
||
|
|
||
|
return $decoded_result;
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
__END__
|
||
|
|
||
|
=pod
|
||
|
|
||
|
=head1 NAME
|
||
|
|
||
|
MetaCPAN::API::Tiny - A Tiny API client for MetaCPAN
|
||
|
|
||
|
=head1 VERSION
|
||
|
|
||
|
version 1.131730
|
||
|
|
||
|
=head1 DESCRIPTION
|
||
|
|
||
|
This is the Tiny version of L<MetaCPAN::API>. It implements a compatible API
|
||
|
with a few notable exceptions:
|
||
|
|
||
|
=over 4
|
||
|
|
||
|
=item Attributes are direct hash access
|
||
|
|
||
|
The attributes defined using Mo(o|u)se are now accessed via the blessed hash
|
||
|
directly. There are no accessors defined to access this elements.
|
||
|
|
||
|
=item Exception handling
|
||
|
|
||
|
Instead of using Try::Tiny, raw evals are used. This could potentially cause
|
||
|
issues, so just be aware.
|
||
|
|
||
|
=item Testing
|
||
|
|
||
|
Test::Fatal was replaced with an eval implementation of exception().
|
||
|
Test::TinyMocker usage is retained, but may be absorbed since it is pure perl
|
||
|
|
||
|
=back
|
||
|
|
||
|
=head1 CLASS_METHODS
|
||
|
|
||
|
=head2 new
|
||
|
|
||
|
new is the constructor for MetaCPAN::API::Tiny. In the non-tiny version of this
|
||
|
module, this is provided via Any::Moose built from the attributes defined. In
|
||
|
the tiny version, we define our own constructor. It takes the same arguments
|
||
|
and provides similar checks to MetaCPAN::API with regards to arguments passed.
|
||
|
|
||
|
=head1 PUBLIC_METHODS
|
||
|
|
||
|
=head2 source
|
||
|
|
||
|
my $source = $mcpan->source(
|
||
|
author => 'DOY',
|
||
|
release => 'Moose-2.0201',
|
||
|
path => 'lib/Moose.pm',
|
||
|
);
|
||
|
|
||
|
Searches MetaCPAN for a module or a specific release and returns the plain source.
|
||
|
|
||
|
=head2 release
|
||
|
|
||
|
my $result = $mcpan->release( distribution => 'Moose' );
|
||
|
|
||
|
# or
|
||
|
my $result = $mcpan->release( author => 'DOY', release => 'Moose-2.0001' );
|
||
|
|
||
|
Searches MetaCPAN for a dist.
|
||
|
|
||
|
You can do complex searches using 'search' parameter:
|
||
|
|
||
|
# example lifted from MetaCPAN docs
|
||
|
my $result = $mcpan->release(
|
||
|
search => {
|
||
|
author => "OALDERS AND ",
|
||
|
filter => "status:latest",
|
||
|
fields => "name",
|
||
|
size => 1,
|
||
|
},
|
||
|
);
|
||
|
|
||
|
=head2 pod
|
||
|
|
||
|
my $result = $mcpan->pod( module => 'Moose' );
|
||
|
|
||
|
# or
|
||
|
my $result = $mcpan->pod(
|
||
|
author => 'DOY',
|
||
|
release => 'Moose-2.0201',
|
||
|
path => 'lib/Moose.pm',
|
||
|
);
|
||
|
|
||
|
Searches MetaCPAN for a module or a specific release and returns the POD.
|
||
|
|
||
|
=head2 module
|
||
|
|
||
|
my $result = $mcpan->module('MetaCPAN::API');
|
||
|
|
||
|
Searches MetaCPAN and returns a module's ".pm" file.
|
||
|
|
||
|
=head2 file
|
||
|
|
||
|
A synonym of L</module>
|
||
|
|
||
|
=head2 author
|
||
|
|
||
|
my $result1 = $mcpan->author('XSAWYERX');
|
||
|
my $result2 = $mcpan->author( pauseid => 'XSAWYERX' );
|
||
|
|
||
|
Searches MetaCPAN for a specific author.
|
||
|
|
||
|
You can do complex searches using 'search' parameter:
|
||
|
|
||
|
# example lifted from MetaCPAN docs
|
||
|
my $result = $mcpan->author(
|
||
|
search => {
|
||
|
q => 'profile.name:twitter',
|
||
|
size => 1,
|
||
|
},
|
||
|
);
|
||
|
|
||
|
=head2 fetch
|
||
|
|
||
|
my $result = $mcpan->fetch('/release/distribution/Moose');
|
||
|
|
||
|
# with parameters
|
||
|
my $more = $mcpan->fetch(
|
||
|
'/release/distribution/Moose',
|
||
|
param => 'value',
|
||
|
);
|
||
|
|
||
|
This is a helper method for API implementations. It fetches a path from MetaCPAN, decodes the JSON from the content variable and returns it.
|
||
|
|
||
|
You don't really need to use it, but you can in case you want to write your own extension implementation to MetaCPAN::API.
|
||
|
|
||
|
It accepts an additional hash as "GET" parameters.
|
||
|
|
||
|
=head2 post
|
||
|
|
||
|
# /release&content={"query":{"match_all":{}},"filter":{"prefix":{"archive":"Cache-Cache-1.06"}}}
|
||
|
my $result = $mcpan->post(
|
||
|
'release',
|
||
|
{
|
||
|
query => { match_all => {} },
|
||
|
filter => { prefix => { archive => 'Cache-Cache-1.06' } },
|
||
|
},
|
||
|
);
|
||
|
|
||
|
The POST equivalent of the "fetch()" method. It gets the path and JSON request.
|
||
|
|
||
|
=head1 THANKS
|
||
|
|
||
|
Overall the tests and code were ripped directly from MetaCPAN::API and
|
||
|
tiny-fied. A big thanks to Sawyer X for writing the original module.
|
||
|
|
||
|
=head1 AUTHOR
|
||
|
|
||
|
Nicholas R. Perez <nperez@cpan.org>
|
||
|
|
||
|
=head1 COPYRIGHT AND LICENSE
|
||
|
|
||
|
This software is copyright (c) 2013 by Nicholas R. Perez <nperez@cpan.org>.
|
||
|
|
||
|
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
|
||
|
METACPAN_API_TINY
|
||
|
|
||
|
s/^ //mg for values %fatpacked;
|
||
|
|
||
|
unshift @INC, sub {
|
||
|
if (my $fat = $fatpacked{$_[1]}) {
|
||
|
if ($] < 5.008) {
|
||
|
return sub {
|
||
|
return 0 unless length $fat;
|
||
|
$fat =~ s/^([^\n]*\n?)//;
|
||
|
$_ = $1;
|
||
|
return 1;
|
||
|
};
|
||
|
}
|
||
|
open my $fh, '<', \$fat
|
||
|
or die "FatPacker error loading $_[1] (could be a perl installation issue?)";
|
||
|
return $fh;
|
||
|
}
|
||
|
return
|
||
|
};
|
||
|
|
||
|
} # END OF FATPACK CODE
|
||
|
|
||
|
|
||
|
use 5.010;
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
use Fatal qw(open close);
|
||
|
|
||
|
use Getopt::Long;
|
||
|
use Pod::Usage;
|
||
|
use File::Basename;
|
||
|
use File::Path qw(make_path);
|
||
|
use Module::CoreList;
|
||
|
use HTTP::Tiny;
|
||
|
use Safe;
|
||
|
use MetaCPAN::API::Tiny;
|
||
|
use Digest::SHA qw(sha256_hex);
|
||
|
use Text::Wrap;
|
||
|
$Text::Wrap::columns = 62;
|
||
|
|
||
|
# Below, 5.036 should be aligned with the version of perl actually
|
||
|
# bundled in Buildroot:
|
||
|
die <<"MSG" if $] < 5.036;
|
||
|
This script needs a host perl with the same major version as Buildroot target perl.
|
||
|
|
||
|
Your current host perl is:
|
||
|
$^X
|
||
|
version $]
|
||
|
|
||
|
You may install a local one by running:
|
||
|
perlbrew install perl-5.36.0
|
||
|
MSG
|
||
|
|
||
|
my ($help, $man, $quiet, $force, $recommend, $test, $host);
|
||
|
my $target = 1;
|
||
|
GetOptions( 'help|?' => \$help,
|
||
|
'man' => \$man,
|
||
|
'quiet|q' => \$quiet,
|
||
|
'force|f' => \$force,
|
||
|
'host!' => \$host,
|
||
|
'target!' => \$target,
|
||
|
'recommend' => \$recommend,
|
||
|
'test' => \$test
|
||
|
) or pod2usage(-exitval => 1);
|
||
|
pod2usage(-exitval => 0) if $help;
|
||
|
pod2usage(-exitval => 0, -verbose => 2) if $man;
|
||
|
pod2usage(-exitval => 1) if scalar @ARGV == 0;
|
||
|
|
||
|
my %dist; # name -> metacpan data
|
||
|
my %need_target; # name -> 1 if target package is needed
|
||
|
my %need_host; # name -> 1 if host package is needed
|
||
|
my %need_dlopen; # name -> 1 if requires dynamic library
|
||
|
my %is_xs; # name -> 1 if XS module
|
||
|
my %deps_build; # name -> list of host dependencies
|
||
|
my %deps_runtime; # name -> list of target dependencies
|
||
|
my %license_files; # name -> hash of license files
|
||
|
my %checksum; # author -> list of checksum
|
||
|
my $mirror = 'https://cpan.metacpan.org'; # a CPAN mirror
|
||
|
my $mcpan = MetaCPAN::API::Tiny->new(base_url => 'https://fastapi.metacpan.org/v1');
|
||
|
my $ua = HTTP::Tiny->new();
|
||
|
my $new_pkgs;
|
||
|
|
||
|
my %white_list = (
|
||
|
'ExtUtils-Config' => 1,
|
||
|
'ExtUtils-InstallPaths' => 1,
|
||
|
'ExtUtils-Helpers' => 1,
|
||
|
'File-ShareDir-Install' => 1,
|
||
|
'Module-Build' => 1,
|
||
|
'Module-Build-Tiny' => 1,
|
||
|
);
|
||
|
my @info = ();
|
||
|
|
||
|
sub get_checksum {
|
||
|
my ($url) = @_;
|
||
|
my ($path) = $url =~ m|^[^:/?#]+://[^/?#]*([^?#]*)|;
|
||
|
my ($basename, $dirname) = fileparse( $path );
|
||
|
unless ($checksum{$dirname}) {
|
||
|
my $url = $mirror . $dirname . q{CHECKSUMS};
|
||
|
my $response = $ua->get($url);
|
||
|
$checksum{$dirname} = $response->{content};
|
||
|
}
|
||
|
my $chksum = Safe->new->reval($checksum{$dirname});
|
||
|
return $chksum->{$basename}, $basename;
|
||
|
}
|
||
|
|
||
|
sub is_xs {
|
||
|
my ($manifest) = @_;
|
||
|
# This heuristic determines if a module is a native extension, by searching
|
||
|
# some file extension types in the MANIFEST of the distribution.
|
||
|
# It was inspired by http://deps.cpantesters.org/static/purity.html
|
||
|
return $manifest =~ m/\.(swg|xs|c|h|i)[\n\s]/;
|
||
|
}
|
||
|
|
||
|
sub find_license_files {
|
||
|
my ($manifest) = @_;
|
||
|
my @license_files;
|
||
|
foreach (split /\n/, $manifest) {
|
||
|
next if m|/|;
|
||
|
s|\s+.*$||;
|
||
|
push @license_files, $_ if m/(ARTISTIC|COPYING|COPYRIGHT|GPL\S*|LICENSE|LICENCE)/i;
|
||
|
}
|
||
|
if (scalar @license_files == 0 && $manifest =~ m/(README)[\n\s]/i) {
|
||
|
@license_files = ($1);
|
||
|
}
|
||
|
if (scalar @license_files == 0 && $manifest =~ m/(README\.md)[\n\s]/i) {
|
||
|
@license_files = ($1);
|
||
|
}
|
||
|
if (scalar @license_files == 0 && $manifest =~ m/(README\.pod)[\n\s]/i) {
|
||
|
@license_files = ($1);
|
||
|
}
|
||
|
return @license_files;
|
||
|
}
|
||
|
|
||
|
sub want_test {
|
||
|
my ($distname) = @_;
|
||
|
return 1 if $need_dlopen{$distname} && scalar @{$deps_runtime{$distname}} > 0;
|
||
|
}
|
||
|
|
||
|
sub get_dependencies {
|
||
|
my ($distname) = @_;
|
||
|
my %dep = map { $_ => 1 } @{$deps_runtime{$distname}};
|
||
|
for my $direct (@{$deps_runtime{$distname}}) {
|
||
|
for (get_dependencies( $direct )) {
|
||
|
$dep{$_} = 1;
|
||
|
}
|
||
|
}
|
||
|
return keys %dep;
|
||
|
}
|
||
|
|
||
|
sub get_indirect_dependencies {
|
||
|
my ($distname) = @_;
|
||
|
my %indirect;
|
||
|
my %direct = map { $_ => 1 } @{$deps_runtime{$distname}};
|
||
|
for my $dep (get_dependencies( $distname )) {
|
||
|
$indirect{$dep} = 1 unless exists $direct{$dep};
|
||
|
}
|
||
|
return keys %indirect;
|
||
|
}
|
||
|
|
||
|
sub fetch {
|
||
|
my ($name, $need_target, $need_host, $top) = @_;
|
||
|
$need_target{$name} = $need_target if $need_target;
|
||
|
$need_host{$name} = $need_host if $need_host;
|
||
|
unless ($dist{$name} && !$top) {
|
||
|
say qq{fetch ${name}} unless $quiet;
|
||
|
my $result = $mcpan->release( distribution => $name );
|
||
|
my $main_module = $result->{main_module};
|
||
|
push @info, qq{[$name] $main_module is a core module}
|
||
|
if $top && Module::CoreList::is_core( $main_module, undef, $] );
|
||
|
$dist{$name} = $result;
|
||
|
$license_files{$name} = {};
|
||
|
eval {
|
||
|
my $author = $result->{author};
|
||
|
my $release = $name . q{-} . $result->{version};
|
||
|
my $manifest = $mcpan->source( author => $author, release => $release, path => 'MANIFEST' );
|
||
|
$need_dlopen{$name} = $is_xs{$name} = is_xs( $manifest );
|
||
|
foreach my $fname (find_license_files( $manifest )) {
|
||
|
my $license = $mcpan->source( author => $author, release => $release, path => $fname );
|
||
|
$license_files{$name}->{$fname} = sha256_hex( $license );
|
||
|
}
|
||
|
};
|
||
|
if ($@) {
|
||
|
warn $@;
|
||
|
}
|
||
|
my %build = ();
|
||
|
my %runtime = ();
|
||
|
my %optional = ();
|
||
|
foreach my $dep (@{$result->{dependency}}) {
|
||
|
my $modname = ${$dep}{module};
|
||
|
next if $modname eq q{perl};
|
||
|
next if $modname =~ m|^Alien|;
|
||
|
next if $modname =~ m|^Win32|;
|
||
|
next if !($test && $top) && $modname =~ m|^Test|;
|
||
|
next if Module::CoreList::is_core( $modname, undef, $] );
|
||
|
# we could use the host Module::CoreList data, because host perl and
|
||
|
# target perl have the same major version
|
||
|
next if ${$dep}{phase} eq q{develop};
|
||
|
next if ${$dep}{phase} eq q{x_Dist_Zilla};
|
||
|
next if !($test && $top) && ${$dep}{phase} eq q{test};
|
||
|
my $distname = $mcpan->module( $modname )->{distribution};
|
||
|
if (${$dep}{phase} eq q{runtime}) {
|
||
|
if (${$dep}{relationship} eq q{requires}) {
|
||
|
$runtime{$distname} = 1;
|
||
|
}
|
||
|
else {
|
||
|
$optional{$distname} = 1 if $recommend && $top;
|
||
|
}
|
||
|
}
|
||
|
else { # configure, build
|
||
|
$build{$distname} = 1;
|
||
|
push @info, qq{[$name] suspicious dependency on $distname}
|
||
|
unless exists $white_list{$distname};
|
||
|
}
|
||
|
}
|
||
|
$deps_build{$name} = [keys %build];
|
||
|
$deps_runtime{$name} = [keys %runtime];
|
||
|
foreach my $distname (@{$deps_build{$name}}) {
|
||
|
fetch( $distname, 0, 1 );
|
||
|
}
|
||
|
foreach my $distname (@{$deps_runtime{$name}}) {
|
||
|
fetch( $distname, $need_target, $need_host );
|
||
|
$need_dlopen{$name} ||= $need_dlopen{$distname};
|
||
|
}
|
||
|
foreach my $distname (keys %optional) {
|
||
|
fetch( $distname, $need_target, $need_host );
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
foreach my $distname (@ARGV) {
|
||
|
# Command-line's distributions
|
||
|
fetch( $distname, !!$target, !!$host, 1 );
|
||
|
}
|
||
|
say scalar keys %dist, q{ packages fetched.} unless $quiet;
|
||
|
|
||
|
# Buildroot package name: lowercase
|
||
|
sub fsname {
|
||
|
my $name = shift;
|
||
|
$name =~ s|_|-|g;
|
||
|
return q{perl-} . lc $name;
|
||
|
}
|
||
|
|
||
|
# Buildroot variable name: uppercase
|
||
|
sub brname {
|
||
|
my $name = shift;
|
||
|
$name =~ s|-|_|g;
|
||
|
return uc $name;
|
||
|
}
|
||
|
|
||
|
# Buildroot requires license name as in https://spdx.org/licenses/
|
||
|
sub brlicense {
|
||
|
my $license = shift;
|
||
|
$license =~ s|apache_1_1|Apache-1.1|;
|
||
|
$license =~ s|apache_2_0|Apache-2.0|;
|
||
|
$license =~ s|artistic_2|Artistic-2.0|;
|
||
|
$license =~ s|artistic|Artistic-1.0|;
|
||
|
$license =~ s|lgpl_2_1|LGPL-2.1|;
|
||
|
$license =~ s|lgpl_3_0|LGPL-3.0|;
|
||
|
$license =~ s|gpl_2|GPL-2.0|;
|
||
|
$license =~ s|gpl_3|GPL-3.0|;
|
||
|
$license =~ s|mit|MIT|;
|
||
|
$license =~ s|mozilla_1_1|Mozilla-1.1|;
|
||
|
$license =~ s|openssl|OpenSSL|;
|
||
|
$license =~ s|perl_5|Artistic or GPL-1.0+|;
|
||
|
return $license;
|
||
|
}
|
||
|
|
||
|
while (my ($distname, $dist) = each %dist) {
|
||
|
my $fsname = fsname( $distname );
|
||
|
my $dirname = q{package/} . $fsname;
|
||
|
my $cfgname = $dirname . q{/Config.in};
|
||
|
my $mkname = $dirname . q{/} . $fsname . q{.mk};
|
||
|
my $hashname = $dirname . q{/} . $fsname . q{.hash};
|
||
|
my $brname = brname( $fsname );
|
||
|
my $testdir = q{support/testing/tests/package};
|
||
|
my $testname = $testdir . q{/test_} . lc $brname . q{.py};
|
||
|
unless (-d $dirname) {
|
||
|
make_path $dirname;
|
||
|
$new_pkgs = 1;
|
||
|
}
|
||
|
if ($need_target{$distname} && ($force || !-f $cfgname)) {
|
||
|
$dist->{abstract} =~ s|\s+$||;
|
||
|
$dist->{abstract} .= q{.} unless $dist->{abstract} =~ m|\.$|;
|
||
|
my $abstract = wrap( q{}, qq{\t }, $dist->{abstract} );
|
||
|
my $homepage = $dist->{resources}->{homepage} || qq{https://metacpan.org/release/${distname}};
|
||
|
say qq{write ${cfgname}} unless $quiet;
|
||
|
open my $fh, q{>}, $cfgname;
|
||
|
say {$fh} qq{config BR2_PACKAGE_${brname}};
|
||
|
say {$fh} qq{\tbool "${fsname}"};
|
||
|
say {$fh} qq{\tdepends on !BR2_STATIC_LIBS} if $need_dlopen{$distname};
|
||
|
foreach my $dep (sort @{$deps_runtime{$distname}}) {
|
||
|
my $brdep = brname( fsname( $dep ) );
|
||
|
say {$fh} qq{\tselect BR2_PACKAGE_${brdep} # runtime};
|
||
|
}
|
||
|
say {$fh} qq{\thelp};
|
||
|
say {$fh} qq{\t ${abstract}\n} if $abstract;
|
||
|
say {$fh} qq{\t ${homepage}};
|
||
|
if ($need_dlopen{$distname}) {
|
||
|
say {$fh} qq{\ncomment "${fsname} needs a toolchain w/ dynamic library"};
|
||
|
say {$fh} qq{\tdepends on BR2_STATIC_LIBS};
|
||
|
}
|
||
|
close $fh;
|
||
|
}
|
||
|
if ($force || !-f $mkname) {
|
||
|
my $version = $dist->{version};
|
||
|
my ($path) = $dist->{download_url} =~ m|^[^:/?#]+://[^/?#]*([^?#]*)|;
|
||
|
# this URL contains only the scheme, auth and path parts (but no query and fragment parts)
|
||
|
# the scheme is not used, because the job is done by the BR download infrastructure
|
||
|
# the auth part is not used, because we use $(BR2_CPAN_MIRROR)
|
||
|
my ($filename, $directories, $suffix) = fileparse( $path, q{tar.gz}, q{tgz} );
|
||
|
$directories =~ s|/$||;
|
||
|
my @dependencies = map( { q{host-} . fsname( $_ ); } sort @{$deps_build{$distname}} );
|
||
|
my $dependencies = join qq{ \\\n\t}, @dependencies;
|
||
|
$dependencies = qq{\\\n\t} . $dependencies if scalar @dependencies > 1;
|
||
|
my @host_dependencies = map { q{host-} . fsname( $_ ); } sort( @{$deps_build{$distname}},
|
||
|
@{$deps_runtime{$distname}} );
|
||
|
my $host_dependencies = join qq{ \\\n\t}, @host_dependencies;
|
||
|
$host_dependencies = qq{\\\n\t} . $host_dependencies if scalar @host_dependencies > 1;
|
||
|
my $license = brlicense( ref $dist->{license} eq 'ARRAY'
|
||
|
? join q{ or }, @{$dist->{license}}
|
||
|
: $dist->{license} );
|
||
|
my $license_files = join q{ }, sort keys %{$license_files{$distname}};
|
||
|
if ($license_files && (!$license || $license eq q{unknown})) {
|
||
|
push @info, qq{[$distname] undefined LICENSE, see $license_files};
|
||
|
$license = q{???};
|
||
|
}
|
||
|
say qq{write ${mkname}} unless $quiet;
|
||
|
open my $fh, q{>}, $mkname;
|
||
|
say {$fh} qq{################################################################################};
|
||
|
say {$fh} qq{#};
|
||
|
say {$fh} qq{# ${fsname}};
|
||
|
say {$fh} qq{#};
|
||
|
say {$fh} qq{################################################################################};
|
||
|
say {$fh} qq{};
|
||
|
say {$fh} qq{${brname}_VERSION = ${version}};
|
||
|
say {$fh} qq{${brname}_SOURCE = ${distname}-\$(${brname}_VERSION).${suffix}};
|
||
|
say {$fh} qq{${brname}_SITE = \$(BR2_CPAN_MIRROR)${directories}};
|
||
|
say {$fh} qq{${brname}_DEPENDENCIES = ${dependencies}} if $need_target{$distname} && $dependencies;
|
||
|
say {$fh} qq{HOST_${brname}_DEPENDENCIES = ${host_dependencies}} if $need_host{$distname} && $host_dependencies;
|
||
|
say {$fh} qq{${brname}_LICENSE = ${license}} if $license;
|
||
|
say {$fh} qq{${brname}_LICENSE_FILES = ${license_files}} if $license_files;
|
||
|
say {$fh} qq{${brname}_DISTNAME = ${distname}};
|
||
|
say {$fh} qq{};
|
||
|
say {$fh} qq{\$(eval \$(perl-package))} if $need_target{$distname};
|
||
|
say {$fh} qq{\$(eval \$(host-perl-package))} if $need_host{$distname};
|
||
|
close $fh;
|
||
|
}
|
||
|
if ($force || !-f $hashname) {
|
||
|
my ($checksum, $filename) = get_checksum($dist->{download_url});
|
||
|
my $md5 = $checksum->{md5};
|
||
|
my $sha256 = $checksum->{sha256};
|
||
|
say qq{write ${hashname}} unless $quiet;
|
||
|
open my $fh, q{>}, $hashname;
|
||
|
say {$fh} qq{# retrieved by scancpan from ${mirror}/};
|
||
|
say {$fh} qq{md5 ${md5} ${filename}};
|
||
|
say {$fh} qq{sha256 ${sha256} ${filename}};
|
||
|
my %license_files = %{$license_files{$distname}};
|
||
|
if (scalar keys %license_files) {
|
||
|
say {$fh} q{};
|
||
|
say {$fh} qq{# computed by scancpan};
|
||
|
foreach my $license (sort keys %license_files) {
|
||
|
my $digest = $license_files{$license};
|
||
|
say {$fh} qq{sha256 ${digest} ${license}};
|
||
|
}
|
||
|
}
|
||
|
close $fh;
|
||
|
}
|
||
|
if (want_test( $distname ) && ($force || !-f $testname)) {
|
||
|
my $classname = $distname;
|
||
|
$classname =~ s|-||g;
|
||
|
my $modname = $distname;
|
||
|
$modname =~ s|-|::|g;
|
||
|
my $mark = $is_xs{$distname} ? q{ XS} : q{};
|
||
|
my @indirect = (get_indirect_dependencies( $distname ));
|
||
|
say qq{write ${testname}} unless $quiet;
|
||
|
make_path $testdir unless -d $testdir;
|
||
|
open my $fh, q{>}, $testname;
|
||
|
say {$fh} qq{from tests.package.test_perl import TestPerlBase};
|
||
|
say {$fh} qq{};
|
||
|
say {$fh} qq{};
|
||
|
say {$fh} qq{class TestPerl${classname}(TestPerlBase):};
|
||
|
say {$fh} qq{ """};
|
||
|
say {$fh} qq{ package:};
|
||
|
say {$fh} qq{ ${distname}${mark}};
|
||
|
say {$fh} qq{ direct dependencies:};
|
||
|
foreach my $dep (sort @{$deps_runtime{$distname}}) {
|
||
|
$mark = $is_xs{$dep} ? q{ XS} : q{};
|
||
|
say {$fh} qq{ ${dep}${mark}};
|
||
|
}
|
||
|
if (scalar @indirect > 0) {
|
||
|
say {$fh} qq{ indirect dependencies:};
|
||
|
foreach my $dep (sort @indirect) {
|
||
|
$mark = $is_xs{$dep} ? q{ XS} : q{};
|
||
|
say {$fh} qq{ ${dep}${mark}};
|
||
|
}
|
||
|
}
|
||
|
say {$fh} qq{ """};
|
||
|
say {$fh} qq{};
|
||
|
say {$fh} qq{ config = TestPerlBase.config + \\};
|
||
|
say {$fh} qq{ """};
|
||
|
say {$fh} qq{ BR2_PACKAGE_PERL=y};
|
||
|
say {$fh} qq{ BR2_PACKAGE_${brname}=y};
|
||
|
say {$fh} qq{ """};
|
||
|
say {$fh} qq{};
|
||
|
say {$fh} qq{ def test_run(self):};
|
||
|
say {$fh} qq{ self.login()};
|
||
|
foreach my $dep (sort grep { $is_xs{$_} } @indirect) {
|
||
|
$dep =~ s|-|::|g;
|
||
|
say {$fh} qq{ self.module_test("${dep}")};
|
||
|
}
|
||
|
foreach my $dep (sort grep { $is_xs{$_} } @{$deps_runtime{$distname}}) {
|
||
|
$dep =~ s|-|::|g;
|
||
|
say {$fh} qq{ self.module_test("${dep}")};
|
||
|
}
|
||
|
say {$fh} qq{ self.module_test("${modname}")};
|
||
|
close $fh;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($new_pkgs) {
|
||
|
my %pkg;
|
||
|
my $cfgname = q{package/Config.in};
|
||
|
if (-f $cfgname) {
|
||
|
open my $fh, q{<}, $cfgname;
|
||
|
while (<$fh>) {
|
||
|
chomp;
|
||
|
$pkg{$_} = 1 if m|package/perl-|;
|
||
|
}
|
||
|
close $fh;
|
||
|
}
|
||
|
|
||
|
foreach my $distname (keys %need_target) {
|
||
|
my $fsname = fsname( $distname );
|
||
|
$pkg{qq{\tsource "package/${fsname}/Config.in"}} = 1;
|
||
|
}
|
||
|
|
||
|
say qq{${cfgname} must contain the following lines:};
|
||
|
say join qq{\n}, sort keys %pkg;
|
||
|
}
|
||
|
|
||
|
say join qq{\n}, @info;
|
||
|
|
||
|
__END__
|
||
|
|
||
|
=head1 NAME
|
||
|
|
||
|
utils/scancpan Try-Tiny Moo
|
||
|
|
||
|
=head1 SYNOPSIS
|
||
|
|
||
|
utils/scancpan [options] [distname ...]
|
||
|
|
||
|
Options:
|
||
|
-help
|
||
|
-man
|
||
|
-quiet
|
||
|
-force
|
||
|
-target/-notarget
|
||
|
-host/-nohost
|
||
|
-recommend
|
||
|
-test
|
||
|
|
||
|
=head1 OPTIONS
|
||
|
|
||
|
=over 8
|
||
|
|
||
|
=item B<-help>
|
||
|
|
||
|
Prints a brief help message and exits.
|
||
|
|
||
|
=item B<-man>
|
||
|
|
||
|
Prints the manual page and exits.
|
||
|
|
||
|
=item B<-quiet>
|
||
|
|
||
|
Executes without output
|
||
|
|
||
|
=item B<-force>
|
||
|
|
||
|
Forces the overwriting of existing files.
|
||
|
|
||
|
=item B<-target/-notarget>
|
||
|
|
||
|
Switches package generation for the target variant (the default is C<-target>).
|
||
|
|
||
|
=item B<-host/-nohost>
|
||
|
|
||
|
Switches package generation for the host variant (the default is C<-nohost>).
|
||
|
|
||
|
=item B<-recommend>
|
||
|
|
||
|
Adds I<recommended> dependencies.
|
||
|
|
||
|
=item B<-test>
|
||
|
|
||
|
Adds dependencies for test.
|
||
|
|
||
|
=back
|
||
|
|
||
|
=head1 DESCRIPTION
|
||
|
|
||
|
This script creates templates of the Buildroot package files for all the
|
||
|
Perl/CPAN distributions required by the specified distnames. The
|
||
|
dependencies and metadata are fetched from https://metacpan.org/.
|
||
|
|
||
|
After running this script, it is necessary to check the generated files.
|
||
|
For distributions that link against a target library, you have to add the
|
||
|
buildroot package name for that library to the DEPENDENCIES variable.
|
||
|
|
||
|
See the Buildroot documentation for details on the usage of the Perl
|
||
|
infrastructure.
|
||
|
|
||
|
The major version of the host perl must be aligned on the target one,
|
||
|
in order to work with the right CoreList data.
|
||
|
|
||
|
=head1 LICENSE
|
||
|
|
||
|
Copyright (C) 2013-2023 by Francois Perrad <francois.perrad@gadz.org>
|
||
|
|
||
|
This program is free software; you can redistribute it and/or modify
|
||
|
it under the terms of the GNU General Public License as published by
|
||
|
the Free Software Foundation; either version 2 of the License, or
|
||
|
(at your option) any later version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License
|
||
|
along with this program; if not, write to the Free Software
|
||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||
|
|
||
|
This script is a part of Buildroot.
|
||
|
|
||
|
This script requires the module C<MetaCPAN::API::Tiny> (version 1.131730)
|
||
|
which was included at the beginning of this file by the tool C<fatpack>.
|
||
|
|
||
|
See L<https://metacpan.org/release/NPEREZ/MetaCPAN-API-Tiny-1.131730>.
|
||
|
|
||
|
See L<https://metacpan.org/release/App-FatPacker>.
|
||
|
|
||
|
These both libraries are free software and may be distributed under the same
|
||
|
terms as perl itself.
|
||
|
|
||
|
And perl may be distributed under the terms of Artistic v1 or GPL v1 license.
|
||
|
|
||
|
=cut
|