pipless/pipgen.py

198 lines
6.1 KiB
Python
Executable File

#!/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("/")[2]}-{pkg_data["version"]}.ebuild', 'w') as fh:
fh.write(fill_template(pkg_data))
if __name__ == '__main__':
__main__()