From de04f65d0fadd3c1f075969190b0f3ad7a8c5bef Mon Sep 17 00:00:00 2001 From: Nemo Date: Mon, 26 Oct 2020 23:23:20 +0530 Subject: [PATCH] Generate PKGBUILD from pip packages --- pip2arch.py | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100755 pip2arch.py diff --git a/pip2arch.py b/pip2arch.py new file mode 100755 index 0000000..9e4a398 --- /dev/null +++ b/pip2arch.py @@ -0,0 +1,252 @@ +#!/usr/bin/python +from __future__ import unicode_literals +from __future__ import with_statement + +import sys +import datetime +import logging +import argparse +import re + +# make this script work for python2 and python3 +# the try will fail on python3 +try: + input = raw_input + from xmlrpclib import ServerProxy +except NameError: + from xmlrpc.client import ServerProxy + +BLANK_PKGBUILD = """\ +#Automatically generated by pip2arch on {date} +pkgname={pkg.outname} +pkgver={pkg.version} +pkgrel=1 +pkgdesc="{pkg.description}" +url="{pkg.url}" +depends=('{pkg.pyversion}' {depends}) +makedepends=('{pkg.distributepackage}' {makedepends}) +license=('{pkg.license}') +arch=('any') +source=('{pkg.download_url}') +md5sums=('{pkg.md5}') +build() {{ + cd $srcdir/{pkg.name}-{pkg.version} + {pkg.pyversion} setup.py build +}} +package() {{ + cd $srcdir/{pkg.name}-{pkg.version} + {pkg.pyversion} setup.py install --root="$pkgdir" --optimize=1 {pkg.setup_args} +}} +""" + +SOURCEFILE_TYPE_RE = re.compile(".*\.(tar|zip|gz|z|bz2?|xz)", re.IGNORECASE) + +class pip2archException(Exception): pass +class VersionNotFound(pip2archException): pass +class LackOfInformation(pip2archException): pass + +class Package(object): + logging.info('Creating Server Proxy object') + client = ServerProxy('https://pypi.python.org/pypi') + depends = [] + makedepends = [] + data_received = False + setup_args = '' + + def get_package(self, name, outname, pyversion ,version=None): + if version is None: + versions = self.client.package_releases(name) + if len(versions) > 1: + version = self.choose_version(versions) + else: + logging.info('Using version %s' % versions[0]) + version = versions[0] + self.version = version + self.pyversion = pyversion + + data = self.client.release_data(name, version) + logging.info('Got release_data from PyPi') + + raw_urls = self.client.release_urls(name, version) + logging.info('Got release_urls from PyPi') + if not len(data): + raise VersionNotFound('PyPi did not return any information for version {0}'.format(self.version)) + elif not len(raw_urls): + if 'download_url' in data: + download_url = data['download_url'] + if SOURCEFILE_TYPE_RE.match(data['download_url']) is None: + raise LackOfInformation("Couldn't find any suitable source") + else: + urls = {'url': download_url} + logging.warning('Got download link but no md5, you may have to search it by youself or generate it') + else: + raise LackOfInformation('PyPi did not return the necessary information to create the PKGBUILD') + else: + urls = {} + for url in raw_urls: + if SOURCEFILE_TYPE_RE.match(url['filename']): + urls = url + if not urls: + raise pip2archException + ('Selected package version had no suitable sources') + logging.info('Parsed release_urls data') + + + self.distributepackage = 'python2-distribute' if\ + self.pyversion != 'python' else 'python3' + logging.info("Set distribute package as {0}".format(self.distributepackage)) + + if outname is not None: + self.outname = outname.lower() + elif any(re.search(r'Librar(ies|y)', item) for item in data['classifiers']): + #if this is a library + self.outname = '{pyversion}-{pkgname}'.format( + pyversion=self.pyversion, pkgname=name).lower() + logging.info('Automaticly added {0} to the front of the package'.format(self.pyversion)) + else: + self.outname = name.lower() + + #check for licenes texts + if len(data.get('license', '')) > 10: + self.license = 'CUSTOM' + else: + self.license = data.get('license', 'UNKNOWN') + + try: + self.name = data['name'] + self.description = data['summary'] + self.download_url = urls.get('url', '') + self.md5 = urls.get('md5_digest', '') + self.url = data.get('home_page', '') + self.depends = data.get('requires', []) + except KeyError: + raise pip2archException('PyPi did not return needed information') + logging.info('Parsed other data') + self.data_received = True + + def search(self, term, interactive=False): + results = self.client.search({'description': term[1:], + 'name': term[1:]}, + 'or') + logging.info('Got search results for term {term} from PyPi server'.format(term=term)) + + #If no results + if not results: + print ('No packages found') + return + for i, result in enumerate(results): + i += 1 + print ('{index}. {name} - {summary}'.format(index=i, name=result['name'], summary=result['summary'])) + + #If we don't want talking, exit here + if not interactive: + #self.data_received = False + return + + selection = raw_input('Enter the number of the PyPi package you would like to process\n') + + try: + selection = int(selection.strip()) + selection -= 1 + chosen = results[selection] + except (TypeError, IndexError): + print ('Not a valid selection. Must be integer in range 1 - {length}'.format(length=len(results))) + retry = raw_input('Retry? [Y/n]\n') + if retry.strip()[0].lower() != 'n': + #offer recurse on failure, maybe user will be smarter this time -.- + return self.search(term) + else: + return + + name = chosen['name'] + outname = chosen['name'] + + return self.get_package(name, outname) + + def choose_version(self, versions): + print ('Multiple versions found:') + print (', '.join(versions)) + ver = raw_input('Which version would you like to use? ') + if ver in versions: + return ver + else: + print ('That was NOT one of the choices...') + print ('Try again') + return self.choose_version(versions) + + def add_depends(self, depends): + self.depends += depends + + def add_makedepends(self, makedepends): + self.makedepends += makedepends + + def render(self): + depends = "'" + "' '".join(d for d in self.depends) + "'" if self.depends else '' + makedepends = "'" + "' '".join(d for d in self.makedepends) + "'" if self.makedepends else '' + return BLANK_PKGBUILD.format(pkg=self, + date=datetime.date.today(), + depends=depends, + makedepends=makedepends) + +def set_logging_level(level_str): + level = getattr(logging, level_str.upper()) + logging.root.setLevel(level) + +def main(): + parser = argparse.ArgumentParser(description='Convert a PyPi package into an Arch Linux PKGBUILD.') + parser.add_argument('pkgname', metavar='PKGNAME', action='store', + help='Name of PyPi package for pip2arch to process') + parser.add_argument('-v', '--version', dest='version', action='store', + help='The version of the speciied PyPi package to process') + parser.add_argument('-p', '--python-version', dest='pyversion', action='store', + default='python', choices=('python','python2'), + help='The python version to build and install the package with') + parser.add_argument('-o', '--output', dest='outfile', action='store', + default='PKGBUILD', + help='The file to output the generated PKGBUILD to') + parser.add_argument('-s', '--search', dest='search', action='store_true', + help="Search for given package name, instead of building PKGBUILD") + parser.add_argument('-i', '--interactive', dest='interactive', action='store_true', + help="Makes all commands interactive, prompting user for input.") + parser.add_argument('-d', '--dependencies', dest='depends', action='append', + help="The name of a package that should be added to the depends array") + parser.add_argument('-m', '--make-dependencies', dest='makedepends', action='append', + help="The name of a package that should be added to the makedepends array") + parser.add_argument('-n', '--output-package-name', dest='outname', action='store', default=None, + help='The name of the package that pip2arch will generate') + parser.add_argument('--logging-level', dest='logging_level', action='store', default='warning', choices=('warning', 'info', 'debug'), + help='The level of logging messages to show') + parser.add_argument('-b', '--build-args', dest='build_args', action='store', + help='Custom arguments for the python install setup.py file') + + args = parser.parse_args() + + set_logging_level(args.logging_level) + + p = Package() + + if args.search: + p.search(args.pkgname, interactive=args.interactive) + else: + p.get_package(name=args.pkgname, pyversion= args.pyversion, + version=args.version, outname=args.outname) + + if args.depends: + p.add_depends(args.depends) + if args.makedepends: + p.add_makedepends(args.makedepends) + if args.build_args: + p.setup_args = args.build_args + if p.data_received: + print ('Got package information') + with open(args.outfile, 'w') as f: + f.write(p.render()) + print ('PKGBUILD written') + +if __name__ == '__main__': + try: + main() + except pip2archException as e: + sys.exit('Pip2Arch error: {0}'.format(e)) + else: + sys.exit(0) \ No newline at end of file