diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/pipgen.py b/pipgen.py new file mode 100755 index 0000000..6c6c8f7 --- /dev/null +++ b/pipgen.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# PyPI ebuild autogenerator, written by ~keith +import requests +import mock +import setuptools +import subprocess +import os +import shutil +import traceback +import importlib.util +import pprint +import re +import argparse + +def get_versions(name: str) -> list: + resp = requests.get(f'https://pypi.org/pypi/{name}/json') + json = resp.json() + return sorted(json['releases'].keys()) + +def get_setuptools_deps(exec_dir: str) -> list: + old_dir = os.getcwd() + try: + with mock.patch.object(setuptools, 'setup') as mock_setup: + spec = importlib.util.spec_from_file_location('setup', exec_dir.rstrip('/') + '/setup.py') + module = importlib.util.module_from_spec(spec) + os.chdir(exec_dir) + spec.loader.exec_module(module) + _, kwargs = mock_setup.call_args + return kwargs.get('install_requires', []) + finally: + os.chdir(old_dir) + +def pkg_exists(gentoo_name: str) -> bool: + return os.path.exists('/var/db/repos/gentoo/' + gentoo_name) or os.path.exists('/var/db/repos/pipless/' + gentoo_name) + +RE_PIP_DEP = re.compile(r'^([a-zA-Z0-9._-]+)\s*(\[[a-zA-Z0-9._,\s-]+\])?\s*(?:(<=?|!=|===?|>=?|~=)\s*([a-zA-Z0-9._*+!-]+))?(\s*,.*)?') +def translate_pip_dep(pip_dep: str) -> str: + match = RE_PIP_DEP.match(pip_dep) + if not match: + print(f'WARN: cannot parse pip_dep: {pip_dep}') + return 'UNTRANSLATABLE: ' + pip_dep + + name = match.group(1) + py_useflags = match.group(2) + version_cmp = match.group(3) + version = match.group(4) + comma = match.group(5) + + if ';' in pip_dep: + print(f'WARN: ignoring environment markers for {name}: {pip_dep.partition(";")[2]}') + if comma: + print(f'WARN: ignoring extra constraints for {name}: {comma.partition(";")[0]}') + if py_useflags: + print(f'WARN: ignoring py_useflags for {name}: {py_useflags}') + + gentoo_name = 'dev-python/' + name.replace('_', '-').replace('.', '-') + if not pkg_exists(gentoo_name): + if pkg_exists(gentoo_name.lower()): + gentoo_name = gentoo_name.lower() + elif pkg_exists('dev-python/' + name): + gentoo_name = 'dev-python/' + name + elif pkg_exists('dev-python/' + name.lower()): + gentoo_name = 'dev-python/' + name.lower() + else: + print(f'WARN: gentoo package not found for {name}, defaulting to {gentoo_name}') + + if not version_cmp: + return gentoo_name + elif version_cmp in ('==', '==='): + return f'={gentoo_name}-{version}' + elif version_cmp == '~=': + return f'>={gentoo_name}-{version}' + else: + return f'{version_cmp}{gentoo_name}-{version}' + +def get_package(name: str) -> dict: + resp = requests.get(f'https://pypi.org/pypi/{name}/json') + json = resp.json() + + git_url = json['info']['project_urls'].get('Source Code', json['info']['home_page']).rstrip('/') + '.git' + + pkg_data = { + 'pypi_name': json['info']['name'], + 'version': json['info']['version'], + 'description': json['info']['summary'], + 'pypi_url': json['info']['package_url'], + 'home_url': json['info']['home_page'], + 'license': json['info']['license'], + 'git_url': git_url, + 'setuptools_deps': [], + 'dependencies': [], + } + + print(f"Got {pkg_data['pypi_name']} version {pkg_data['version']}") + + try: + print("Attempting to find dependencies...") + os.mkdir('TEMP_WORK_DIR') + + tarball_url = None + for url in json['urls']: + if url['url'].endswith('.tar.gz'): + tarball_url = url['url'] + break + assert tarball_url, "tarball not found" + + print("Downloading tarball...") + tarball_path = 'TEMP_WORK_DIR/' + tarball_url.split('/')[-1] + with requests.get(tarball_url, stream=True) as r: + with open(tarball_path, 'wb') as fh: + shutil.copyfileobj(r.raw, fh) + + print("Extracting...") + subprocess.run(['tar', '-xzf', tarball_path, '-C', 'TEMP_WORK_DIR']) + + print("Hooking setup.py...") + pkg_dir = 'TEMP_WORK_DIR/' + tarball_url.split('/')[-1].rpartition('.tar')[0] + assert os.path.exists(pkg_dir + '/setup.py'), f"extracted setup.py not found in {pkg_dir}" + deps = get_setuptools_deps(pkg_dir) + + pkg_data['setuptools_deps'] = deps + except BaseException as e: + print("Error determining package dependencies. YOU WILL HAVE TO MANUALLY SPECIFY RDEPENDS!") + traceback.print_exception(e) + finally: + print("Cleaning up...") + if os.path.exists('TEMP_WORK_DIR'): + shutil.rmtree('TEMP_WORK_DIR') + + for dep in pkg_data['setuptools_deps']: + pkg_data['dependencies'].append(translate_pip_dep(dep)) + + return pkg_data + +def fill_template(pkg_data: dict) -> str: + ebuild = f''' + # Ebuild for {pkg_data['pypi_name']} + # Auto-generated by pipgen.py - TEST ME! + EAPI=8 + + DISTUTILS_USE_PEP517=setuptools + PYTHON_COMPAT=( python3_{{8..11}} pypy3 ) + + inherit distutils-r1 + + DESCRIPTION="{pkg_data['description']}" + HOMEPAGE=" + {pkg_data['home_url']} + {pkg_data['pypi_url']} + " + + MY_PN="{pkg_data['pypi_name']}" + MY_P="${{MY_PN}}-${{PV}}" + + if [[ "${{PV}}" == *9999* ]]; then + EGIT_REPO_URI="{pkg_data['git_url']}" + inherit git-r3 + else + SRC_URI="mirror://pypi/${{MY_P:0:1}}/${{MY_PN}}/${{MY_P}}.tar.gz" + KEYWORDS="~alpha ~amd64 ~arm ~arm64 ~hppa ~ia64 ~loong ~mips ~ppc ~ppc64 ~riscv ~s390 ~sparc ~x86" + S="${{WORKDIR}}/${{MY_P}}" + fi + + LICENSE="{pkg_data['license']}" + SLOT="0" + IUSE="" + ''' + ebuild = '\n'.join(line[1:] if line.startswith('\t') else line for line in ebuild.split('\n')) + "\n" + ebuild += 'RDEPEND="\n' + for pkg in pkg_data['dependencies']: + ebuild += f"\t{pkg}[${{PYTHON_USEDEP}}]\n" + ebuild += '"\n' + + ebuild += "# pkg_data:\n" + for line in pprint.pformat(pkg_data).split('\n'): + ebuild += f"# {line}\n" + + return ebuild + +def __main__(): + parser = argparse.ArgumentParser(description='auto-generate ebuilds from pypi packages') + parser.add_argument('name', help='pypi package name') + parser.add_argument('version', nargs='?', default=None, help='package version (latest if unspecified)') + parser.add_argument('-O', '--output', default=None, help='desired gentoo package name') + + args = parser.parse_args() + + pkg_name = args.output or 'dev-python/' + args.name.replace('_', '-').replace('.', '-') + if not os.path.exists(pkg_name): + os.makedirs(pkg_name) + + pkg_data = get_package((args.name + '/' + args.version) if args.version else args.name) + with open(f'{pkg_name}/{pkg_name.partition("/")[0]}-{pkg_data["version"]}.ebuild', 'w') as fh: + fh.write(fill_template(pkg_data)) + +if __name__ == '__main__': + __main__()