#!/usr/bin/perl # apk add perl-libwww perl-json use strict; use warnings; use 5.016; use feature "switch"; no if $] >= 5.018, warnings => "experimental::smartmatch"; use LWP::UserAgent; use LWP::ConnCache; use CPAN::Meta; use Module::CoreList; use JSON; my $license_mappings = { "perl_5" => "GPL-1.0-or-later OR Artistic-1.0-Perl", "artistic_2" => "Artistic-2.0", "gpl_3" => "GPL-3.0-only", }; my $package_mappings = { "LWP" => "perl-libwww", "TermReadKey" => "perl-term-readkey", "perl-ldap" => "perl-ldap", }; our $packager = ""; my $template = <<'EOF'; # Automatically generated by apkbuild-cpan, template 3 [% authors %] pkgname=[% pkgname %] _pkgreal=[% pkgreal %] pkgver=[% pkgver %] pkgrel=0 pkgdesc="Perl module for [% pkgreal %]" url="https://metacpan.org/release/[% pkgreal %]/" arch="noarch" license="GPL-1.0-or-later OR Artistic-1.0-Perl" depends="perl" makedepends="perl-dev" checkdepends="" subpackages="$pkgname-doc" source="[% source %]" builddir="$srcdir/$_pkgreal-$pkgver" build() { : } check() { : } package() { : } EOF our $ua = LWP::UserAgent->new(); our $json = JSON->new; $ua->env_proxy; $ua->conn_cache(LWP::ConnCache->new()); sub read_file { my ($filename) = @_; local $/; open my $fh, "<", $filename or die "could not open $filename: $!"; return <$fh>; } sub read_assignments_from_file { my ($filename) = @_; return () if ( ! -e $filename ); my $text = read_file($filename); my %sline = $text =~ /^(\w+)\s*=\s*([^\"\n]*)$/mg; my %mline = $text =~ /^(\w+)\s*=\s*\"([^\"]*)\"$/mg; my %hash = ( %sline, %mline ); my $authors = join("\n", $text =~ /^# Contributor: .*$/mg, $text =~ /^# Maintainer: .*$/mg); $hash{'authors'} = $authors if length($authors) > 1; my $tmplver = $text =~ m/^# Automatically generated by apkbuild-cpan, template (.*)/; $hash{'tmplver'} = $1 if length($tmplver) >= 1; return \%hash; } sub map_cpan_to_apk { my ($cpan_distrib) = @_; return $package_mappings->{$cpan_distrib} if exists $package_mappings->{$cpan_distrib}; # most packages are named according to the # distribution name return 'perl-' . lc $cpan_distrib; } sub read_apkbuild { return read_assignments_from_file("APKBUILD"); } sub write_apkbuild { my ($distdata, $authors, $moddata) = @_; my $cpanid = $distdata->{id}; $cpanid = substr($cpanid, 0, 1) . "/" . substr($cpanid, 0, 2) . "/$cpanid"; my %repl = ( authors => ($authors or "# Contributor: $packager\n# Maintainer: $packager"), pkgname => map_cpan_to_apk($moddata->{distribution}), pkgreal => $moddata->{distribution}, pkgver => $moddata->{version} =~ s/^[^0-9]+//r, source => $moddata->{download_url}, pkgdesc => $distdata->{abstract}, ); $repl{source} =~ s/$repl{pkgver}/\$pkgver/g; $template =~ s/\[% (.*?) %\]/$repl{$1}/g; open my $fh, '>', "APKBUILD" or die; print $fh $template; close $fh; say "Wrote $repl{pkgname}/APKBUILD"; } sub parse_deps { my ($reqs) = @_; my $distfiles = {}; my $response; my $deps = ""; foreach $reqs (@_) { for my $module ($reqs->required_modules) { if (Module::CoreList->is_core($module)) { my $perlver = Module::CoreList->first_release($module); say "$module is part of core perl since $perlver."; next; } next if $module eq 'perl'; # map module name to package name $response = $ua->get("https://fastapi.metacpan.org/module/$module"); $response->is_success or die $response->status_line; my $moddata = $json->decode($response->decoded_content); $moddata->{error} and die "Error trying to locate $module: $moddata->{error}\n"; $distfiles->{$module} = $moddata->{distribution}; } } # map package names to alpine packages foreach ( keys %{ $distfiles } ) { $response = $ua->get("https://fastapi.metacpan.org/module/$_"); $response->is_success or die $response->status_line; my $distdata = $json->decode($response->decoded_content); $distdata->{error} and die "Error trying to locate $_: $distdata->{error}\n"; my $pkgname = map_cpan_to_apk($distdata->{distribution}); $deps .= "$pkgname " unless $deps =~ m/$pkgname/; } $deps =~ s/\h+/ /g; $deps =~ s/ $//; return $deps; } sub prepare_tree { system("abuild checksum unpack prepare") == 0 or die "abuild checksum failed"; } sub update_functions { my $apkbuild = read_apkbuild; my $metaprefix = "src/" . $apkbuild->{'_pkgreal'} . "-" . $apkbuild->{'pkgver'} . "/"; my $build_func; my $check_func; my $package_func; my $text = read_file "APKBUILD"; if (-e "$metaprefix/Build.PL" ) { $build_func = <<'EOF'; build() { export CFLAGS=$(perl -MConfig -E 'say $Config{ccflags}') perl Build.PL installdirs=vendor ./Build } EOF $package_func = <<'EOF'; package() { ./Build install destdir="$pkgdir" find "$pkgdir" \( -name perllocal.pod -o -name .packlist \) -delete } EOF $check_func = <<'EOF'; check() { ./Build test } EOF } else { $build_func = <<'EOF'; build() { export CFLAGS=$(perl -MConfig -E 'say $Config{ccflags}') PERL_MM_USE_DEFAULT=1 perl -I. Makefile.PL INSTALLDIRS=vendor make } EOF $package_func = <<'EOF'; package() { make DESTDIR="$pkgdir" install find "$pkgdir" \( -name perllocal.pod -o -name .packlist \) -delete } EOF $check_func = <<'EOF'; check() { export CFLAGS=$(perl -MConfig -E 'say $Config{ccflags}') make test } EOF } $text =~ s/^build\(\) \{.*?^\}\n/$build_func/smg or die "Can't replace build function APKBUILD"; $text =~ s/^package\(\) \{.*?^\}\n/$package_func/smg or die "Can't replace package function APKBUILD"; $text =~ s/^check\(\) \{.*?^\}\n/$check_func/smg or die "Can't replace check function APKBUILD"; open my $fh, '>', "APKBUILD" or die; print $fh $text; close $fh; } sub do_depends { my $apkbuild = read_apkbuild; my $metaprefix = ''; if ( exists $apkbuild->{'_realname'} ) { $metaprefix = "src/" . $apkbuild->{'_realname'} . "-" . $apkbuild->{'pkgver'} . "/"; } elsif ( exists $apkbuild->{'_pkgreal'} ) { $metaprefix = "src/" . $apkbuild->{'_pkgreal'} . "-" . $apkbuild->{'pkgver'} . "/"; } elsif ( exists $apkbuild->{'_pkgname'} ) { $metaprefix = "src/" . $apkbuild->{'_pkgname'} . "-" . $apkbuild->{'pkgver'} . "/"; } elsif ( exists $apkbuild->{'_name'} ) { $metaprefix = "src/" . $apkbuild->{'_name'} . "-" . $apkbuild->{'pkgver'} . "/"; } elsif ( exists $apkbuild->{'_realpkgname'} ) { $metaprefix = "src/" . $apkbuild->{'_realpkgname'} . "-" . $apkbuild->{'pkgver'} . "/"; } elsif ( exists $apkbuild->{'_pkg_real'} ) { $metaprefix = "src/" . $apkbuild->{'_pkg_real'} . "-" . $apkbuild->{'pkgver'} . "/"; } else { die "Unable to find meta file directory - check APKBUILD Perl Module Name"; } $metaprefix =~ s/-\$pkgver//g; my $meta; foreach my $metafile ("MYMETA.json", "META.json", "MYMETA.yml", "META.yml") { if (-e "$metaprefix$metafile") { say "Using meta information from $metafile"; $meta = CPAN::Meta->load_file("$metaprefix$metafile"); last; } } die "No dependency meta file found" unless $meta; my $abstract = $meta->abstract; say "Abstract: $abstract"; my $license = join " ", map {$license_mappings->{$_} or $_} $meta->license; say "License: $license"; my $deps = parse_deps $meta->effective_prereqs->requirements_for('runtime', 'requires'); if ($deps eq '') { $deps = "perl"; } else { $deps = "perl " . $deps; } say "CPAN deps: $deps"; say "Recommend: " . parse_deps $meta->effective_prereqs->requirements_for('runtime', 'recommends'); my $makedeps = parse_deps( $meta->effective_prereqs->requirements_for('configure', 'requires'), $meta->effective_prereqs->requirements_for('configure', 'recommends'), $meta->effective_prereqs->requirements_for('build', 'requires'), $meta->effective_prereqs->requirements_for('build', 'recommends') ); if ($makedeps eq '') { $makedeps = "perl-dev"; } else { $makedeps = "perl-dev " . $makedeps; } say "CPAN build deps: $makedeps"; say "CPAN requires: " . parse_deps($meta->effective_prereqs->requirements_for('configure', 'requires'), $meta->effective_prereqs->requirements_for('build', 'requires')); say "CPAN recommends: " . parse_deps($meta->effective_prereqs->requirements_for('configure', 'recommends'), $meta->effective_prereqs->requirements_for('build', 'recommends')); my $checkdeps = parse_deps($meta->effective_prereqs->requirements_for('test', 'requires'), $meta->effective_prereqs->requirements_for('test', 'recommends')); say "CPAN check deps: $checkdeps"; my $text = read_file "APKBUILD"; if ($abstract && $abstract ne 'unknown') { $text =~ s/^pkgdesc=\"([^\"]*)\"$/pkgdesc=\"$abstract\"/mg or die "Can't find pkgdesc line in APKBUILD"; } if (length(`find $metaprefix -name '*.xs'`)) { $text =~ s/^arch=\"([^\"]*)\"$/arch="all"/mg or die "Can't find arch line in APKBUILD"; } if ($license ne 'unknown') { $text =~ s/^license=\"([^\"]*)\"$/license=\"$license\"/mg or die "Can't find license line in APKBUILD"; } $text =~ s/^depends=\"([^\"]*)\"$/depends=\"$deps\"/mg or $text =~ s/(license=.*\n)/$1depends=\"$deps\"\n/gm or die "Can't insert depends line in APKBUILD"; $text =~ s/^makedepends=\"([^\"]*)\"$/makedepends=\"$makedeps\"/mg or $text =~ s/(depends=.*\n)/$1makedepends=\"$makedeps\"\n/gm or die "Can't insert makedepends line in APKBUILD"; $text =~ s/^checkdepends=\"([^\"]*)\"$/checkdepends=\"$checkdeps\"/mg or $text =~ s/(makedepends=.*\n)/$1checkdepends=\"$checkdeps\"\n/gm or die "Can't insert checkdepends line in APKBUILD"; # remove empty variables $text =~ s/.*=""\n//g; open my $fh, '>', "APKBUILD" or die; print $fh $text; close $fh; } sub get_data { my $apkbuild = read_apkbuild; my $pkgreal = ''; if (exists $apkbuild->{_realname}) { $pkgreal = $apkbuild->{_realname}; } elsif (exists $apkbuild->{_pkgreal}) { $pkgreal = $apkbuild->{_pkgreal}; } elsif (exists $apkbuild->{_pkgname}) { $pkgreal = $apkbuild->{_pkgname}; } elsif (exists $apkbuild->{_name}) { $pkgreal = $apkbuild->{_name}; } elsif (exists $apkbuild->{_realpkgname}) { $pkgreal = $apkbuild->{_realpkgname}; } elsif (exists $apkbuild->{_pkg_real}) { $pkgreal = $apkbuild->{_pkg_real}; } else { my $module = ''; my $distribution = ''; while ((my $key, my $value ) = each (%$apkbuild)) { # Do not parse any depends lines to not find incorrect module if ($key =~ m/.*depends.*/) { next; } # Try to find a perl module name in APKBUILD if ($value=~m/((\w+::)+\w+)/g) { # Match Perl Module names containing :: $module .= "$1 " unless $module =~ m/$1/; } elsif ($value =~ m/(([A-Z]\w+-)+\w+)/) { # Match possible distribution names with - $distribution .= "$1 " unless $distribution =~ m/ *$1 /; } elsif ( $value =~ m/.*release\/([A-Z]\w+).*/ ) { # Match Single Word Perl Module Name after release in URL? $distribution .= "$1 " unless $distribution =~ m/ *$1 /; } } # Want to try the traditional Module::Name first my $list = $module . $distribution; foreach (split / /, $list) { my $type = ''; if( $_ =~ m/::/ ) { $type = 'module'; } else { $type = 'release'; } my $response = $ua->get("https://fastapi.metacpan.org/$type/$_"); $response->is_success or next;; my $moddata = $json->decode($response->decoded_content); $moddata->{error} and next; $pkgreal = $moddata->{distribution}; last; } } $pkgreal =~ s/-\$pkgver//g; my $response = $ua->get("https://fastapi.metacpan.org/release/$pkgreal"); $response->is_success or die $response->status_line . " unable to find $pkgreal verify Perl Module name in APKBUILD\n"; my $distdata = $json->decode($response->decoded_content); $distdata->{error} and die "Error trying to locate $pkgreal: $distdata->{error}\n"; $response = $ua->get("https://fastapi.metacpan.org/module/$distdata->{main_module}"); $response->is_success or die $response->status_line; my $moddata = $json->decode($response->decoded_content); $moddata->{error} and die "Error trying to locate $distdata->{main_module}: $moddata->{error}\n"; return ($apkbuild, $distdata, $moddata); } my $abuild_conf = read_assignments_from_file("/etc/abuild.conf"); $packager = $abuild_conf->{PACKAGER} if $abuild_conf->{PACKAGER}; my $user_abuild_conf = read_assignments_from_file($ENV{"HOME"} . "/.abuild/abuild.conf"); $packager = $user_abuild_conf->{PACKAGER} if $user_abuild_conf->{PACKAGER}; given ( $ARGV[0] ) { when ("create") { my $module = $ARGV[1]; $module or die "Module name is a mandatory argument"; my $response = $ua->get("https://fastapi.metacpan.org/module/$module"); $response->is_success or die $response->status_line; my $moddata = $json->decode($response->decoded_content); $moddata->{error} and die "Error trying to locate $module: $moddata->{error}\n"; $response = $ua->get("https://fastapi.metacpan.org/release/$moddata->{distribution}"); $response->is_success or die $response->status_line; my $distdata = $json->decode($response->decoded_content); $distdata->{error} and die "Error trying to locate $module: $distdata->{error}\n"; my $apkname = map_cpan_to_apk $distdata->{metadata}{name}; mkdir $apkname; chdir $apkname; write_apkbuild($distdata, undef, $moddata); prepare_tree; update_functions; do_depends; } when ("recreate") { #TODO: likely should keep pkgrel the same on recreate my ($apkbuild, $distdata, $moddata) = get_data; write_apkbuild($distdata, $apkbuild->{authors}, $moddata); prepare_tree; update_functions; do_depends; } when ("upgrade") { my ($apkbuild, $distdata, $moddata) = get_data; my $pkgver = $moddata->{version} =~ s/^[^0-9]+//r; if ($pkgver ne $apkbuild->{pkgver}) { say "Upgrading CPAN module from $apkbuild->{pkgver} to $pkgver"; my $text = read_file "APKBUILD"; $text =~ s/^pkgver=(.*)$/pkgver=$pkgver/mg or die "Can't find pkgver line in APKBUILD"; $text =~ s/^pkgrel=(.*)$/pkgrel=0/mg; open my $fh, '>', "APKBUILD" or die; say $fh $text; close $fh; prepare_tree; do_depends; } else { say "Up-to-data with CPAN"; } } when ('check') { my ($apkbuild, $distdata, $moddata) = get_data; my $pkgver = $moddata->{version} =~ s/^[^0-9]+//r; say "$apkbuild->{pkgname}: Latest version: $pkgver Packaged version: $apkbuild->{pkgver}"; if ($pkgver ne $apkbuild->{pkgver}) { exit(1); } } when ("update") { prepare_tree; do_depends; } default { say "Usage: apkbuild-cpan [create | check | recreate | update | upgrade]"; exit; } }