Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
466 lines (370 sloc) 11.6 KB
#!/usr/bin/env perl
use strict;
use warnings;
use JSON ();
use CPAN::Version;
use Getopt::Long;
use File::Basename qw(basename);
use CPAN::DistnameInfo;
use Module::CoreList;
use Parse::CPAN::Packages::Fast;
use File::Spec;
use HTTP::Tiny;
my $cpan = 'cpan.org';
my $repo;
my $perl;
my $local;
my $cpanfile;
my $force;
my $stewfile;
my $yaml;
GetOptions(
"cpan=s" => \$cpan,
"repo=s" => \$repo,
"cpanfile=s" => \$cpanfile,
"stewfile=s" => \$stewfile,
"local" => \$local,
"force" => \$force,
"perl=s" => \$perl,
"yaml" => \$yaml
) or die("Error in command line arguments\n");
die '--cpan is required' unless $cpan;
$cpan = "http://$cpan" unless $cpan =~ m{^https?://};
my @packages = @ARGV;
if ($cpanfile) {
require Module::CPANfile;
my $file = Module::CPANfile->load($cpanfile);
my $prereqs = $file->prereqs->merged_requirements->as_string_hash;
push @packages, keys %$prereqs;
}
die "Usage: [options] <package1> <package2>\n" unless @packages;
my $packages_index = create_packages_index($cpan);
foreach my $package (@packages) {
next if $package eq 'perl';
print "Working on '$package'...\n";
my $distribution = resolve_package($package);
if (!$distribution) {
warn "Cannot resolve $package\n";
next;
}
next if $distribution->{distribution} eq 'perl';
if (!$force && -f distribution_to_stewfile($distribution)) {
next;
}
print
"$package -> $distribution->{distribution}-$distribution->{version}\n";
gen_tree($distribution, root => 1);
}
print "\nDone\n";
my %gen_tree_seen;
sub gen_tree {
my ($distribution, %params) = @_;
return if $gen_tree_seen{$distribution->{pathname}}++;
my $meta = distribution_meta($distribution);
foreach my $dependency (@{$meta->{dependencies}}) {
if (!$force && -f distribution_to_stewfile($dependency)) {
next;
}
gen_tree($dependency);
}
warn sprintf "Generating stewfile for %s-%s (%s)\n",
$distribution->{distribution}, $distribution->{version},
distribution_to_stewfile($distribution);
my $ok = gen_stew($distribution, $meta);
if ($ok && $stewfile && $params{root}) {
open my $fh, '>>', $stewfile or die "Can't open stewfile: $!\n";
print $fh dist_to_pkg($distribution->{distribution}), "\n";
close $fh;
}
}
sub dist_to_pkg {
my ($dist) = @_;
my @parts = map { s/_/-/g; $_ } split /-/, $dist;
return 'lib' . join('-', map { lc } @parts) . '-perl';
}
my %gen_stew_seen;
sub gen_stew {
my ($distribution, $meta) = @_;
return if !$force && $gen_stew_seen{$distribution->{pathname}}++;
my $dependencies = $meta->{dependencies};
my $pkg = dist_to_pkg($distribution->{distribution});
my $version = $distribution->{version};
my $dist = $distribution->{distribution};
my $pathname = $distribution->{pathname};
my $file = basename($pathname);
my ($name) = $file =~ m/^(.*)(?:\.tar.gz|\.tgz)$/;
my $stew_file = distribution_to_stewfile($distribution);
my $dist_url = "$cpan/authors/id/$pathname";
if ($local && $repo) {
my $ua = HTTP::Tiny->new;
my $local_path = File::Spec->catfile($repo, 'src', basename($pathname));
print "Mirroring '$dist_url' -> '$local_path'\n";
my $response = $ua->mirror($dist_url, $local_path);
die "Failed: $response->{reason}: $response->{content}\n"
unless $response->{success};
}
my $options = '';
if (!$local) {
$options = qq{\$url = "$dist_url";};
}
my $builder = 'build';
if ($distribution->{distribution} eq 'ExtUtils-MakeMaker'
|| grep { $_->{distribution} eq 'ExtUtils-MakeMaker' } @$dependencies)
{
$builder = 'make_maker';
}
if ($yaml) {
_write_yaml(
$stew_file,
{
name => $name,
pkg => $pkg,
version => $version,
deps => $dependencies,
file => $file,
sources => $local ? $file : $dist_url,
options => $options,
builder => $builder
}
);
}
else {
_write_perl(
$stew_file,
{
name => $name,
pkg => $pkg,
version => $version,
deps => $dependencies,
file => $file,
options => $options
}
);
}
return 1;
}
sub resolve_package {
my ($package) = @_;
my $info = $packages_index->package($package);
return unless $info;
return {
package => $info->package,
distribution => $info->distribution->dist,
version => $info->distribution->version,
pathname => $info->distribution->pathname,
};
}
sub create_packages_index {
my ($cpan) = @_;
my $details_file_url = "$cpan/modules/02packages.details.txt.gz";
my $details_file = basename $details_file_url;
my $ua = HTTP::Tiny->new;
print "Mirroring '$details_file_url' -> '$details_file'...\n";
my $response = $ua->mirror($details_file_url, $details_file);
die "Failed: $response->{reason}: $response->{content}\n"
unless $response->{success};
return Parse::CPAN::Packages::Fast->new($details_file);
}
sub distribution_meta {
my ($distribution) = @_;
my $ua = HTTP::Tiny->new;
my $response = $ua->post(
'http://fastapi.metacpan.org/v1/release/_search',
{
content => JSON::encode_json(
{
query => {match_all => {}},
filter => {
and => [
$distribution->{version}
? (
{
term => {
version => '' . $distribution->{version}
}
},
)
: (),
{
term => {
distribution =>
$distribution->{distribution}
}
}
]
},
size => 1
}
)
}
);
my $result = JSON::decode_json($response->{content});
$result = $result->{hits}->{hits}->[0];
my $runtime =
$result->{_source}->{metadata}->{prereqs}->{runtime}->{requires} // {};
my $configure =
$result->{_source}->{metadata}->{prereqs}->{configure}->{requires} // {};
my $prereqs = {%$runtime, %$configure};
my @dependencies;
my %dependencies_seen;
foreach my $dep_name (sort keys %$prereqs) {
my $module = $packages_index->package($dep_name);
next unless $module;
my $pathname = $module->distribution->pathname;
my $dist = $module->distribution->dist;
next if $gen_tree_seen{$pathname};
next if $gen_stew_seen{$pathname};
next if $dist eq 'perl';
next if $dependencies_seen{$dist}++;
my $core = Module::CoreList->first_release($dep_name, $module->distribution->version);
if ($core && $perl && CPAN::Version->vcmp($perl, $core) > 0) {
warn "Skipping $dep_name (comes with perl-$perl)\n";
$gen_tree_seen{$pathname}++;
$gen_stew_seen{$pathname}++;
next;
}
push @dependencies,
{
distribution => $dist,
package => $dep_name,
pathname => $module->distribution->pathname,
version => $module->distribution->version,
};
}
return {
distribution => $distribution,
dependencies => \@dependencies
};
}
sub distribution_to_stewfile {
my ($distribution) = @_;
my $pkg = dist_to_pkg($distribution->{distribution});
my $stew_file = "${pkg}_$distribution->{version}.stew";
if ($repo) {
$stew_file = File::Spec->catfile($repo, 'stew', $stew_file);
}
return $stew_file;
}
sub _write_yaml {
my ($stew_file, $params) = @_;
my $name = $params->{name};
my $pkg = $params->{pkg};
my $version = $params->{version};
my $deps = $params->{deps};
my $file = $params->{file};
my $sources = $params->{sources};
my $options = $params->{options};
my $builder = $params->{builder};
$deps = join "\n",
map { " - $_" } map { dist_to_pkg($_->{distribution}) } @$deps;
$deps = "\n" . $deps if $deps;
open my $fh, '>', $stew_file or die $!;
print $fh <<"EOF";
---
# Auto generated by cpan2stew
name: $pkg
version: $version
package: {{name}}-{{version}}
sources: $sources
depends:
- perl$deps
prepare:
- cmd: "tar xzf '$file'"
build:
- chdir: $name
- cmd: "unset PERL5LIB PERL_MM_OPT PERL_MB_OPT PERL_LOCAL_LIB_ROOT"
- env:
PERL_MM_USE_DEFAULT: 1
MODULEBUILDRC: /dev/null
PERL_AUTOINSTALL: --skipdeps
PERL5LIB: \${DESTDIR}/{{PREFIX}}/lib/perl5/vendor_perl
EOF
if ($builder eq 'build') {
print $fh <<"EOF";
- cmd: perl Build.PL
- cmd: ./Build
EOF
}
else {
print $fh <<"EOF";
- cmd: perl Makefile.PL
- cmd: make
EOF
}
print $fh <<"EOF";
install:
- chdir: $name
- cmd: "unset PERL5LIB PERL_MM_OPT PERL_MB_OPT PERL_LOCAL_LIB_ROOT"
- env:
PERL5LIB: \${DESTDIR}/{{PREFIX}}/lib/perl5/vendor_perl
EOF
if ($builder eq 'build') {
print $fh <<"EOF";
- cmd: ./Build install --installdirs=vendor --destdir=\${DESTDIR}
EOF
}
else {
print $fh <<"EOF";
- cmd: perl Makefile.PL
- cmd: make install INSTALLDIRS=vendor DESTDIR=\${DESTDIR}
EOF
}
print $fh <<"EOF";
cleanup:
- chdir: \${DESTDIR}
- cmd: "find . -name 'perllocal.pod' -exec rm -f {} \\\\;"
- cmd: "find . -name '.packlist' -exec rm -f {} \\\\;"
- cmd: "find . -type d -empty -delete"
EOF
close $fh;
}
sub _write_perl {
my ($stew_file, $params) = @_;
my $name = $params->{name};
my $pkg = $params->{pkg};
my $version = $params->{version};
my $deps = $params->{deps};
my $file = $params->{file};
my $options = $params->{options};
$deps = join ', ',
map { "'$_'" } map { dist_to_pkg($_->{distribution}) } @$deps;
$deps = ', ' . $deps if $deps;
open my $fh, '>', $stew_file or die $!;
print $fh <<"EOF";
# Auto generated by cpan2stew
\$name = "$pkg";
\$version = "$version";
\$package = "\$name-\$version";
\@depends = ('perl'$deps);
\$file = "$file";
$options
prepare {
"tar xzf '\$file'"
};
build {
"cd $name",
"unset PERL5LIB PERL_MM_OPT PERL_MB_OPT PERL_LOCAL_LIB_ROOT",
"export PERL_MM_USE_DEFAULT=1 MODULEBUILDRC=/dev/null PERL_AUTOINSTALL=--skipdeps",
"export PERL5LIB=\$ENV{DESTDIR}/\$ENV{PREFIX}/lib/perl5/vendor_perl",
(-f '$name/Build.PL' ? 'perl Build.PL' : 'perl Makefile.PL'),
(-f '$name/Build.PL' ? './Build' : 'make')
};
install {
"cd $name",
"unset PERL5LIB PERL_MM_OPT PERL_MB_OPT PERL_LOCAL_LIB_ROOT",
"export PERL5LIB=\$ENV{DESTDIR}/\$ENV{PREFIX}/lib/perl5/vendor_perl",
(
-f '$name/Build.PL'
? "./Build install --installdirs=vendor --destdir=\$ENV{DESTDIR}"
: "make install INSTALLDIRS=vendor DESTDIR=\$ENV{DESTDIR}"
)
};
cleanup {
"cd \$ENV{DESTDIR}",
"find . -name 'perllocal.pod' -exec rm -f {} \\\\;",
"find . -name '.packlist' -exec rm -f {} \\\\;",
"find . -type d -empty -delete",
};
EOF
close $fh;
}