#!/usr/bin/perl use strict; use warnings; use autodie qw(:all); use feature qw(:5.14); use LWP::UserAgent; use LWP::ConnCache; use JSON; use URI; our $packager = ''; my %pkgmap = (); my %licenses = (); my $template = <<'EOF'; # Automatically generated by apkbuild-pypi, template 4 [% authors %] pkgname=[% pkgname %] #_pkgreal is used by apkbuild-pypi to find modules at PyPI _pkgreal=[% pkgreal %] pkgver=[% pkgver %] pkgrel=[% pkgrel %] pkgdesc="[% pkgdesc %]" provides="[% provides %]" replaces="[% replaces %]" url="http://packages.python.org/pypi/[% pkgreal %]" arch="noarch" license="[% license %]" depends="" makedepends="py3-gpep517 py3-setuptools py3-wheel" checkdepends="py3-pytest" subpackages="$pkgname-pyc" source="[% source %]" builddir="$srcdir/$_pkgreal-$pkgver" build() { gpep517 build-wheel \ --wheel-dir .dist \ --output-fd 3 3>&1 >&2 } check() { python3 -m venv --clear --without-pip --system-site-packages .testenv .testenv/bin/python3 -m installer .dist/*.whl .testenv/bin/python3 -m pytest } package() { python3 -m installer -d "$pkgdir" \ .dist/*.whl } EOF my $ua = LWP::UserAgent->new(); my $json = JSON->new; $ua->env_proxy; $ua->conn_cache(LWP::ConnCache->new); sub read_file { my ($filename) = @_; open my $fh, '<:utf8', $filename; local $/; my $text = <$fh>; return $text; } 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_pypi_to_apk { my ($pypi) = @_; return $pkgmap{$pypi} unless !exists($pkgmap{$pypi}); return 'py3-'.lc($pypi); } sub map_license { my ($license) = @_; $license =~ s/ or / /g; return $license; } sub get_source { my ($distdata) = @_; my $pkgname = $distdata->{info}{name}; my $source; for my $url (@{$distdata->{urls}}) { if ($url->{python_version} eq 'source') { $source = URI->new($url->{url}); last; } } die "Unable to locate sources for $pkgname.\n" unless $source; my $filename = ($source->path_segments)[-1]; my $pretty_path = substr($pkgname, 0, 1) . "/$pkgname"; my $pretty_url = $source->clone; $pretty_url->path("/packages/source/$pretty_path/$filename"); my $response = $ua->head($pretty_url); if ($response->is_success) { return $pretty_url->as_string; } else { return $source->as_string; } } sub read_apkbuild { return read_assignments_from_file('APKBUILD'); } sub write_apkbuild { my ($distdata, $apkbuild) = @_; my $replaces = undef; my $provides = undef; my $authors = undef; my $pkgrel = 0; if (defined $apkbuild) { $authors = $apkbuild->{authors}; $provides = $apkbuild->{provides}; $replaces = $apkbuild->{replaces}; $pkgrel = $apkbuild->{pkgrel}; if ($apkbuild->{pkgver} eq $distdata->{info}{version}) { $pkgrel++; } } my %repl = ( authors => ($authors or "# Contributor: $packager\n# Maintainer: $packager"), pkgname => map_pypi_to_apk($distdata->{info}{name}), pkgreal => $distdata->{info}{name}, pkgver => $distdata->{info}{version}, pkgrel => $pkgrel, source => get_source($distdata), license => map_license($distdata->{info}{license}), pkgdesc => $distdata->{info}{summary}, provides => ($provides or ''), replaces => ($replaces or ''), ); $repl{source} =~ s/-$repl{pkgver}/-\$pkgver/g; $template =~ s/\[% (.*?) %\]/$repl{$1}/g; open my $fh, '>:utf8', 'APKBUILD'; print {$fh} $template; close $fh; say "Wrote $repl{pkgname}/APKBUILD"; return \%repl; } sub prepare_tree { system('abuild checksum unpack prepare'); } sub find_package_name { my ($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}; } elsif (exists $apkbuild->{source}) { $pkgreal = $apkbuild->{source}; $pkgreal =~ m/(\w+)-/; $pkgreal = $1; } else { print "No pkg real found\n"; die; } return $pkgreal; } sub get_data { my ($package) = @_; my $response = $ua->get("https://pypi.python.org/pypi/$package/json"); $response->is_success or die $response->status_line; my $distdata = $json->decode($response->decoded_content); return $distdata; } sub get_deps { my ($distdata, $data) = @_; my $reqs = $distdata->{info}{requires_dist}; my %depends = ('py3-pytest' => 'py3-pytest'); my @reqs = map { my $apkname = map_pypi_to_apk(s/(\(|\s+).*$//r); $depends{$apkname} = $apkname } grep { $_ !~ m/; extra ==/ } @$reqs; my @checkdeps = map { my $apkname = map_pypi_to_apk(s/(\(|\s+).*$//r); exists($depends{$apkname}) ? () : $apkname } grep { m/; extra == 'tests'/ } @$reqs; my $apk = read_file('APKBUILD'); $reqs = join ' ', @reqs; $apk =~ s/depends=""/depends="$reqs"/; unshift @checkdeps, 'py3-pytest'; my $checkdeps = join ' ', @checkdeps; $apk =~ s/checkdepends="py3-pytest"/checkdepends="$checkdeps"/; # remove empty variables $apk =~ s/.*=""\n//g; open my $fh, '>:utf8', 'APKBUILD'; print $fh $apk; say "Requires: @reqs\n\nCheckDepends: @checkdeps"; } 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}; sub usage { say <<'EOF'; Usage: apkbuild-pypi [create | check | recreate | upgrade | update] In the repository root: create : Creates an APKBUILD for In the package root: check : Reports current & latest version of the package recreate : Recreates the APKBUILD upgrade : Upgrades to the latest version of the package update : Updates APKBUILD metadata EOF } if (! defined $ARGV[0]) { die usage; } elsif ($ARGV[0] eq 'create') { my $package = $ARGV[1]; $package or die usage; my $distdata = get_data($package); my $apkname = map_pypi_to_apk($package); mkdir $apkname; chdir $apkname; my $data = write_apkbuild($distdata, undef); prepare_tree; get_deps($distdata, $data); } elsif ($ARGV[0] eq 'recreate') { my $apkbuild = read_apkbuild; if (! defined $apkbuild->{_pkgreal}) { $apkbuild->{_pkgreal} = find_package_name($apkbuild); } my $distdata = get_data($apkbuild->{_pkgreal}); my $pkgver = $distdata->{info}{version} =~ s/^[^0-9]+//r; if ($pkgver ne $apkbuild->{pkgver}) { #Reset pkgrel on upgrade on recreate say "Upgrading PyPI module from $apkbuild->{pkgver} to $pkgver"; $apkbuild->{pkgrel}=0; } my $data = write_apkbuild($distdata, $apkbuild); prepare_tree; get_deps($distdata, $data); } elsif ($ARGV[0] eq 'upgrade') { my $apkbuild = read_apkbuild; if (! defined $apkbuild->{_pkgreal}) { $apkbuild->{_pkgreal} = find_package_name($apkbuild); } my $distdata = get_data($apkbuild->{_pkgreal}); my $pkgver = $distdata->{info}{version}; if ($pkgver ne $apkbuild->{pkgver}) { say "Upgrading PyPI package from $apkbuild->{pkgver} to $pkgver"; my $text = read_file('APKBUILD'); $text =~ s/^(pkgver)=.*$/$1=$pkgver/mg or die "Can't find pkgver line in APKBUILD"; $text =~ s/^(pkgrel)=.*$/$1=0/mg or die "Can't find pkgrel line in APKBUILD"; open my $fh, '>:utf8', 'APKBUILD'; print $fh $text; close $fh; } else { say "Already up to date with PyPI"; } } elsif ($ARGV[0] eq 'check') { my $apkbuild = read_apkbuild; if (! defined $apkbuild->{_pkgreal}) { $apkbuild->{_pkgreal} = find_package_name($apkbuild); } my $distdata = get_data($apkbuild->{_pkgreal}); my $pkgver = $distdata->{info}{version}; say "$apkbuild->{pkgname}: Latest version: $pkgver Packaged version: $apkbuild->{pkgver}"; if ($pkgver ne $apkbuild->{pkgver}) { exit(1); } } elsif ($ARGV[0] eq 'update') { prepare_tree; } else { die usage; }