#!/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; use Text::Wrap qw(wrap $columns); use List::Util qw (uniq); use File::Basename qw(dirname); use URI; my $license_mappings = { "perl_5" => "GPL-1.0-or-later OR Artistic-1.0-Perl", "artistic_2" => "Artistic-2.0", "gpl_2" => "GPL-2.0-only", "gpl_3" => "GPL-3.0-only", "mit" => "MIT", "apache_2_0" => "Apache-2.0", }; my $package_mappings = { "LWP" => "perl-libwww", "libwww-perl" => "perl-libwww", "TermReadKey" => "perl-term-readkey", "perl-ldap" => "perl-ldap", "PerlIO-gzip" => "perl-io-gzip", }; our $packager = ""; my $template = <<'EOF'; # Automatically generated by apkbuild-cpan, template 3 [% authors %] pkgname=[% pkgname %] #_pkgreal is used by apkbuild-cpan to find modules at MetaCpan _pkgreal=[% pkgreal %] pkgver=[% pkgver %] pkgrel=[% pkgrel %] pkgdesc="Perl module for [% pkgreal %]" provides="[% provides %]" replaces="[% replaces %]" url="https://metacpan.org/release/[% pkgreal %]/" arch="noarch" license="GPL-1.0-or-later OR Artistic-1.0-Perl" depends="perl" makedepends="" checkdepends="" subpackages="$pkgname-doc" source="[% source %]" builddir="[% builddir %]" 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 $provides = $text =~ m/^provides=\"(.*)\"".*$/mg; $hash{'provides'} = $1 if length($provides) >= 1; my $requires = $text =~ m/^requires=\"(.*)\"".*$/mg; $hash{'requires'} = $1 if length($requires) >= 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, $apkbuild, $moddata ) = @_; my $cpanid = $distdata->{id}; $cpanid = substr( $cpanid, 0, 1 ) . "/" . substr( $cpanid, 0, 2 ) . "/$cpanid"; my $authors = undef; my $replaces = undef; my $provides = undef; my $pkgrel = 0; if ( defined $apkbuild ) { $authors = $apkbuild->{authors}; $provides = $apkbuild->{provides}; $replaces = $apkbuild->{replaces}; $pkgrel = $apkbuild->{pkgrel}; if ( $apkbuild->{pkgver} eq $moddata->{version} ) { $pkgrel++; } } my $builddir; my $srcurl = $moddata->{download_url}; my $filename = (URI->new($srcurl)->path_segments)[-1]; my $pkgreal = $moddata->{distribution}; my $pkgver = $moddata->{version} =~ s/^[^0-9]+//r; if ( $filename =~ m/$pkgreal-$pkgver/ ) { $builddir = "\$srcdir/\$_pkgreal-\$pkgver"; } elsif ( $filename =~ m/$pkgreal-v${pkgver}/ ) { $builddir = "\$srcdir/\$_pkgreal-v\$pkgver"; } else { $builddir = "\$srcdir/\$_pkgreal-" . $moddata->{version}; } my %repl = ( authors => ( $authors or "# Contributor: $packager\n# Maintainer: $packager" ), pkgname => map_cpan_to_apk( $moddata->{distribution} ), pkgreal => $pkgreal, pkgver => $pkgver, pkgrel => $pkgrel, source => $srcurl, pkgdesc => $distdata->{abstract}, provides => ( $provides or '' ), replaces => ( $replaces or '' ), builddir => $builddir, ); $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 = ""; my $reqmodlist = ""; #Get list of unique required modules from all parameters foreach $reqs (@_) { for my $module ( $reqs->required_modules ) { $reqmodlist .= "$module " unless $reqmodlist =~ m/^$module$/; } } # Check add the require module to $dep if not part of core perl foreach my $module ( split " ", $reqmodlist ) { next if $module eq 'perl'; if ( Module::CoreList->is_core($module) ) { my $perlver = Module::CoreList->first_release($module); say " $module is part of core perl since $perlver."; next; } # 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 defined $deps ? $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 sort_pkgs_by_orig { my $sort_order = shift; my $list = shift; return '' if !defined $list; my @array_to_sort = split / /, $list; s/[\t\n\r\f]/ /g for $sort_order; # remove embedded new lines from sort order s/\h+/ /g for $sort_order; # remove embedded spaces from sort order my @sort_order = split / /, $sort_order; #get order of order by string my $count = 0; my %position_of; $position_of{$_} = $count++ for @sort_order; my %sorted; # holds the position of array to sort foreach (@array_to_sort) { my $pos = $position_of{$_}; if ( !defined $pos ) { # not found in sort order so add to the end $sorted{$_} = $count; $count++; } else { $sorted{$_} = $pos; } } my @tmp; # hold array of sorted in position order foreach my $name ( sort { $sorted{$a} <=> $sorted{$b} } keys %sorted ) { push @tmp, $name; } # remove any duplicates uniq @tmp; return join( " ", @tmp ); } sub format_depends { my $deps = shift; return '' if !defined $deps; $columns = 102; $deps =~ s/ {2,}/ /g; $deps =~ s/^\s//g; $deps =~ s/\s$//g; if ( length($deps) >= $columns ) { $deps = wrap( "\t", "\t", $deps ); } $deps =~ s/\s$//g; if ( length($deps) >= $columns ) { $deps = "\n" . $deps . "\n\t"; } return $deps; } sub do_depends { my $modver = shift; my $oldapkbuild = shift; my $apkbuild = read_apkbuild; my $metaprefix = ''; if ( exists $apkbuild->{'_realname'} ) { $metaprefix = "src/" . $apkbuild->{'_realname'}; } elsif ( exists $apkbuild->{'_pkgreal'} ) { $metaprefix = "src/" . $apkbuild->{'_pkgreal'}; } elsif ( exists $apkbuild->{'_pkgname'} ) { $metaprefix = "src/" . $apkbuild->{'_pkgname'}; } elsif ( exists $apkbuild->{'_name'} ) { $metaprefix = "src/" . $apkbuild->{'_name'}; } elsif ( exists $apkbuild->{'_realpkgname'} ) { $metaprefix = "src/" . $apkbuild->{'_realpkgname'}; } elsif ( exists $apkbuild->{'_pkg_real'} ) { $metaprefix = "src/" . $apkbuild->{'_pkg_real'}; } else { die "Unable to find meta file directory - check APKBUILD Perl Module Name"; } $modver = defined($modver) ? $modver : $apkbuild->{'pkgver'}; $modver =~ s/v//g; # try with partial metaprefix my @metafiles = glob(" $metaprefix-*${modver}/*META.json $metaprefix-*${modver}/*META.yml "); if ( @metafiles ) { $metaprefix = dirname($metafiles[0]); } elsif (my $makefile = glob("src/*${modver}/Makefile.PL")) { $metaprefix = dirname($makefile); system("cd $metaprefix && perl -I. Makefile.PL"); } elsif (my $buildpl = glob("src/*${modver}/Build.PL")) { $metaprefix = dirname($buildpl); } else { die "Unable to find meta, makefile, and build.pl - cannot proceed" } # try again with full metaprefix push @metafiles, glob(" $metaprefix/*META.json $metaprefix/*META.yml ") unless @metafiles; my $builddir = do { my $pkgreal = $apkbuild->{'_pkgreal'}; my $oldbuilddir = $apkbuild->{builddir}; my $newbuilddir = $metaprefix; $newbuilddir =~ s/src/\$srcdir/; $newbuilddir =~ s/$pkgreal/\$_pkgreal/; $newbuilddir =~ s/$modver/\$pkgver/; $newbuilddir eq $oldbuilddir ? undef : $newbuilddir }; my $meta; foreach my $metafile ( @metafiles ) { if ( -e "$metafile" ) { say "Using meta information from $metafile"; $meta = CPAN::Meta->load_file("$metafile"); last; } } my $deps; my $abstract; my $license; if ($meta) { $license = join " OR ", map { $license_mappings->{$_} or $_ } $meta->license; say "License: $license"; $abstract = $meta->abstract; say "Abstract: $abstract"; $deps = parse_deps( $meta->effective_prereqs->requirements_for( 'runtime', 'requires' ) ); } if ( defined $oldapkbuild->{'depends'} ) { if ( defined $oldapkbuild->{'cpandepends'} ) { $oldapkbuild->{'depends'} =~ s/\$cpandepends/$oldapkbuild->{'cpandepends'}/g; } my $libs = $oldapkbuild->{'depends'}; $libs =~ s/perl\-\w+[-\w+]+//g; $libs =~ s/perl//g; if ( $libs ne '' ) { $deps = $deps . " " . $libs if defined $deps; } $deps = sort_pkgs_by_orig( $oldapkbuild->{'depends'}, $deps ); } if ( defined $deps && $deps eq '' ) { if (defined $oldapkbuild->{'depends'} && ! $meta ) { $deps = $oldapkbuild->{'depends'}; } else { $deps = "perl"; } } else { $deps = format_depends( "perl " . $deps ) if defined $deps; } my $makedeps = ''; if ($meta) { say "CPAN runtime Required: $deps"; say "CPAN runtime Recommended: " . parse_deps( $meta->effective_prereqs->requirements_for( 'runtime', 'recommends' ) ); $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 ( -e "$metaprefix/Build.PL" ) { $makedeps = "$makedeps perl-module-build" unless $makedeps =~ m/^perl-module-build$/; } } if ( defined $oldapkbuild->{'makedepends'} ) { if ( defined $oldapkbuild->{'cpanmakedepends'} ) { $oldapkbuild->{'makedepends'} =~ s/\$cpanmakedepends/$oldapkbuild->{'cpanmakedepends'}/g; } my $libs = $oldapkbuild->{'makedepends'}; $libs =~ s/perl\-\w+[-\w+]+//g; $makedeps = $makedeps . " " . $libs; $makedeps = sort_pkgs_by_orig( $oldapkbuild->{'makedepends'}, $makedeps ); } my $perldev = ( ( defined $oldapkbuild->{'makedepends'} && $oldapkbuild->{'makedepends'} =~ 'perl-dev' ) || length(`find $metaprefix -name '*.xs'`) ) ? "perl-dev" : ''; if ( defined $makedeps && $makedeps eq '' ) { if (defined $oldapkbuild->{'makedepends'} && ! $meta) { $makedeps = $oldapkbuild->{'makedepends'}; } else { $makedeps = $perldev; } } else { $makedeps = format_depends( $perldev . " " . $makedeps ); } $makedeps =~ s/ +$//g; my $checkdeps; if ($meta) { say "CPAN build deps: $makedeps"; say " CPAN build requires: " . parse_deps( $meta->effective_prereqs->requirements_for( 'configure', 'requires' ), $meta->effective_prereqs->requirements_for( 'build', 'requires' ) ); say " CPAN build recommends: " . parse_deps( $meta->effective_prereqs->requirements_for( 'configure', 'recommends' ), $meta->effective_prereqs->requirements_for( 'build', 'recommends' ) ); $checkdeps = parse_deps( $meta->effective_prereqs->requirements_for( 'test', 'requires' ), $meta->effective_prereqs->requirements_for( 'test', 'recommends' ) ); } if ( defined $oldapkbuild->{'checkdepends'} ) { if ( defined $oldapkbuild->{'cpancheckdepends'} ) { $oldapkbuild->{'checkdepends'} =~ s/\$cpancheckdepends/$oldapkbuild->{'cpancheckdepends'}/g; } my $libs = $oldapkbuild->{'checkdepends'}; $libs =~ s/perl\-\w+[-\w+]+//g; if ( $libs ne "" ) { $checkdeps = $checkdeps . " " . $libs; } $checkdeps = sort_pkgs_by_orig( $oldapkbuild->{'checkdepends'}, $checkdeps ); } if ( defined $checkdeps && $checkdeps eq '' ) { if (defined $oldapkbuild->{'checkdeps'} && ! $meta) { $checkdeps = $oldapkbuild->{'checkdeps'}; } } else { $checkdeps = format_depends($checkdeps); } 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'`) ) { if ( !exists $oldapkbuild->{'arch'} ) { $text =~ s/^arch=\"([^\"]*)\"$/arch="all"/mg or die "Can't find arch line in APKBUILD"; } else { $text =~ s/^arch=\"([^\"]*)\"$/arch="$oldapkbuild->{'arch'}"/mg or die "Can't find arch line in APKBUILD"; } } if ( defined $license && $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\"/gm or die "Can't insert depends line in APKBUILD" if defined $deps; $text =~ s/^makedepends=\"([^\"]*)\"$/makedepends=\"$makedeps\"/mg or $text =~ s/(depends=.*\n)/$1makedepends=\"$makedeps\"/gm or die "Can't insert makedepends line in APKBUILD" if defined $makedeps; $text =~ s/^checkdepends=\"([^\"]*)\"$/checkdepends=\"$checkdeps\"/mg or $text =~ s/(makedepends=.*\n)/$1checkdepends=\"$checkdeps\"/gm or die "Can't insert checkdepends line in APKBUILD" if defined $checkdeps; if (defined $builddir) { print "\n\$builddir redefined:\n\tOLD: '", $apkbuild->{builddir}, "', NEW: '", $builddir, "'\n"; $text =~ s/^builddir=\"([^\"]*)\"/builddir="$builddir"/mg; } # 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}; my $command = $ARGV[0]; if ( $command eq "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 $moddata->{version}; } elsif ( $command eq "recreate" ) { #TODO: likely should keep pkgrel the same on recreate my ( $apkbuild, $distdata, $moddata ) = get_data; my $pkgver = $moddata->{version} =~ s/^[^0-9]+//r; if ( $pkgver ne $apkbuild->{pkgver} ) { #Reset pkgrel on upgrade on recreate say "Upgrading CPAN module from $apkbuild->{pkgver} to $pkgver"; $apkbuild->{pkgrel} = 0; } write_apkbuild( $distdata, $apkbuild, $moddata ); prepare_tree; update_functions; do_depends( $moddata->{version}, $apkbuild ); } elsif ( $command eq "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; #FIXME: review whether this works over time # It deletes the blank line before the checksum # So prepare_tree does not insert extra blank line $text =~ s/\n^(.*sums=.*\n)/$1/mg; open my $fh, '>', "APKBUILD" or die; say $fh $text; close $fh; prepare_tree; do_depends( $moddata->{version}, $apkbuild ); } else { say "Up-to-date with CPAN"; } } elsif ( $command eq "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); } } elsif ( $command eq "update" ) { prepare_tree; do_depends; } else { say "Usage: apkbuild-cpan [create | check | recreate | update | upgrade]"; exit; }