From 2a46aec465207280f31dbe8031f420e8b9851ece Mon Sep 17 00:00:00 2001 From: "A. Wilcox" Date: Wed, 23 May 2018 22:18:34 -0500 Subject: Add APKINDEX reading support --- apkkit/base/index.py | 147 +++++++++++++++++++++++++++++++++++++++++++++++++ apkkit/base/package.py | 103 ++++++++++++++++++++++++++++++++-- setup.py | 9 +-- 3 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 apkkit/base/index.py diff --git a/apkkit/base/index.py b/apkkit/base/index.py new file mode 100644 index 0000000..ee0af9f --- /dev/null +++ b/apkkit/base/index.py @@ -0,0 +1,147 @@ +"""Contains the Index class and related helper classes and functions.""" + +from io import BytesIO +import logging +import os +import requests +import tarfile + +from apkkit.base.package import Package + + +INDEX_LOGGER = logging.getLogger(__name__) + + +class Index: + """The base index class.""" + + def __init__(self, packages=None, description=None, url=None, **kwargs): + """Initialise an Index object. + + :param list packages: + The packages available in the repository this index represents. + + :param str description: + (Recommended) The description of the repository this index + represents. Typically, something like "system" or "user". + + :param str url: + (Optional) The URL to download the index from. All other + parameters are ignored if this is specified. + """ + + if url is not None: + self._url = url + resp = requests.get(url) + if resp.status_code != 200: + INDEX_LOGGER.error("could not download %s: %d (%r)", url, + resp.status_code, resp.text) + else: + self._fill_from_index_file(BytesIO(resp.content)) + else: + if packages is None: + raise ValueError("Packages are required.") + self._pkgs = packages + self._desc = description + + if len(kwargs) > 0: + INDEX_LOGGER.warning("unknown kwargs in Index: %r", kwargs) + + @property + def description(self): + """The description of this repository.""" + return self._desc + + @property + def packages(self): + """The packages available in this repository.""" + return list(self._pkgs) + + @property + def origins(self): + """The names of all unique origin packages available in this repo.""" + return {pkg.origin for pkg in self._pkgs} + + def __repr__(self): + if self._url is not None: + return 'Index(url="{url}")'.format(url=self._url) + return 'Index(packages=<{num} packages>, description="{desc}")'.format( + num=len(self._pkgs), desc=self._desc) + + def to_raw(self): + """Serialises this repository information into the APKINDEX format. + + :returns str: The APKINDEX for this package. Unicode str, ready to be + written to a file. + """ + raise NotImplemented("Not yet.") + + def _fill_from_index_file(self, buf): + """Fill this `Index` object from the APKINDEX in `buf`.""" + if len(getattr(self, '_pkgs', list())) > 0: + raise Exception("Attempt to fill an already filled Index") + + pkgs = list() + params = {} + param_map = {'P': 'name', 'V': 'version', 'A': 'arch', 'o': 'origin', + 'T': 'description', 'p': 'provides', 'i': 'install_if', + 'D': 'depends', 'U': 'url', 'I': 'size', 'r': 'replaces', + 'L': 'license', 'q': 'replaces_priority', 'c': 'commit', + 'm': 'maintainer', 't': 'builddate'} + list_keys = {'p', 'D', 'i', 'r'} + + tar = None + + # assumption: "P" is the first line of each package. + try: + tar = tarfile.open(fileobj=buf) + real_buf = tar.extractfile('APKINDEX') + except: + real_buf = buf + + for line in real_buf.readlines(): + if not isinstance(line, str): + line = line.decode('utf-8') + + # Skip comments. + if line[0] == '#': + continue + + # separated by blank line + if line == "\n" and params.get('name', None) is not None: + pkgs.append(Package(**params)) + params.clear() + continue + + if line.find(':') == -1: + INDEX_LOGGER.warning('!!! malformed line? "%s" !!!', line) + continue + + (key, value) = line.split(':', 1) + key = key.strip() + value = value.strip() + + if key in param_map: + if key in list_keys: + params[param_map[key]] = value.split(' ') + else: + params[param_map[key]] = value + else: + INDEX_LOGGER.info('!!! unrecognised APKINDEX key %s !!!', key) + + self._pkgs = pkgs + + @classmethod + def from_raw(cls, buf): + """Create a new :py:class:`Index` object from an existing APKINDEX. + + :param buf: + The buffer to read from (whether file, BytesIO, etc). + + :returns: + A :py:class:`Index` object with the details from the APKINDEX. + """ + idx = cls(packages=list()) + idx._fill_from_index_file(buf) + return idx + diff --git a/apkkit/base/package.py b/apkkit/base/package.py index a8e1192..109cf04 100644 --- a/apkkit/base/package.py +++ b/apkkit/base/package.py @@ -3,6 +3,7 @@ from jinja2 import Template import logging import os +import time PACKAGE_LOGGER = logging.getLogger(__name__) @@ -10,24 +11,43 @@ PACKAGE_LOGGER = logging.getLogger(__name__) PKGINFO_TEMPLATE = Template(""" # Generated by APK Kit for Adélie Linux -# {{ builduser }}@{{ buildhost }} {{ builddate }} +# {{ builduser }}@{{ buildhost }} {{ self.builddate }} pkgname = {{ package.name }} pkgver = {{ package.version }} pkgdesc = {{ package.description }} arch = {{ package.arch }} size = {{ package.size }} +{%- if package.license %} +license = {{ package.license }} +{%- endif %} {%- if package.url %} url = {{ package.url }} {%- endif %} +{%- if package.origin %} +origin = {{ package.origin }} +{%- endif %} {%- if package.provides %}{%- for provided in package.provides %} provides = {{ provided }} {%- endfor %}{%- endif %} {%- if package.depends %}{%- for depend in package.depends %} depend = {{ depend }} {%- endfor %}{%- endif %} +{%- if package.replaces %}{%- for replace in package.replaces %} +replaces = {{ replace }} +{%- endfor %}{%- endif %} +{%- if package.install_if %}{%- for iif in package.install_if %} +install_if = {{ iif }} +{%- endfor %}{%- endif %} +builddate = {{ builddate }} +{%- if package.commit %} +commit = {{ package.commit }} +{%- endif %} {%- if package.data_hash %} datahash = {{ package.data_hash }} {%- endif %} +{%- if package.maintainer %} +maintainer = {{ package.maintainer }} +{%- endif %} """) """The template used for generating .PKGINFO""" @@ -37,7 +57,9 @@ class Package: """The base package class.""" def __init__(self, name, version, arch, description=None, url=None, size=0, - provides=None, depends=None, **kwargs): + provides=None, depends=None, license=None, origin=None, + replaces=None, commit=None, maintainer=None, builddate=0, + install_if=None, **kwargs): """Initialise a package object. :param str name: @@ -67,6 +89,30 @@ class Package: :param list depends: (Optional) One or more packages that are required to be installed to use this package. + + :param str license: + (Recommended) The license this package is under. + + :param str origin: + (Optional) The origin package, if this package is a subpackage. + Defaults to `name`. + + :param list replaces: + (Optional) One or more packages that this package replaces. + + :param str commit: + (Recommended) The hash of the git commit the repository was on when + this package was built. + + :param str maintainer: + (Recommended) The maintainer of the package. + + :param int builddate: + (Optional) The date the package was built, in UNIX timestamp. + Defaults to right now. + + :param list install_if: + (Optional) Read the APKBUILD.5 manpage. """ self._pkgname = name @@ -77,6 +123,13 @@ class Package: self._arch = arch self._provides = provides or list() self._depends = depends or list() + self._replaces = replaces or list() + self._iif = install_if or list() + self._license = license + self._origin = origin or name + self._commit = commit + self._maintainer = maintainer + self._builddate = builddate or time.time() if '_datahash' in kwargs: self._datahash = kwargs.pop('_datahash') @@ -135,6 +188,36 @@ class Package: """The dependencies of the package.""" return self._depends + @property + def replaces(self): + """The packages this package replaces.""" + return self._replaces + + @property + def install_if(self): + """The packages that pull in this package.""" + return self._iif + + @property + def license(self): + """The license of the package.""" + return self._license + + @property + def origin(self): + """The origin package of this package.""" + return self._origin + + @property + def commit(self): + """The hash of the git commit the build repository was on.""" + return self._commit + + @property + def maintainer(self): + """The maintainer of the package.""" + return self._maintainer + @property def data_hash(self): """The hash of the package's data, or None if not available.""" @@ -148,10 +231,14 @@ class Package: def __repr__(self): return 'Package(name="{name}", version="{ver}", arch="{arch}", '\ 'description="{desc}", url="{url}", size={size}, '\ - 'provides={prov}, depends={dep})'.format( + 'provides={prov}, depends={dep}, license={lic}, '\ + 'origin="{origin}", replaces={rep}, commit="{git}", '\ + 'maintainer="{m}", builddate={ts}, install_if={iif})'.format( name=self._pkgname, ver=self._pkgver, arch=self._arch, desc=self._pkgdesc, prov=self._provides, dep=self._depends, - url=self._url, size=self._size) + url=self._url, size=self._size, lic=self._license, + origin=self._origin, rep=self._replaces, git=self._commit, + m=self._maintainer, ts=self._builddate, iif=self._iif) def to_pkginfo(self): """Serialises the package's information into the PKGINFO format. @@ -184,11 +271,15 @@ class Package: param_map = {'pkgname': 'name', 'pkgver': 'version', 'arch': 'arch', 'pkgdesc': 'description', 'provides': 'provides', 'depend': 'depends', 'url': 'url', 'size': 'size', - 'datahash': '_datahash'} - list_keys = {'provides', 'depend'} + 'replaces': 'replaces', 'builddate': 'builddate', + 'license': 'license', 'datahash': '_datahash', + 'maintainer': 'maintainer', 'commit': 'commit', + 'install_if': 'install_if'} + list_keys = {'provides', 'depend', 'replaces', 'install_if'} params['provides'] = list() params['depends'] = list() + params['replaces'] = list() for line in buf.readlines(): if not isinstance(line, str): diff --git a/setup.py b/setup.py index 119d2e8..78d1575 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup +from setuptools import setup, find_packages from codecs import open from os import path @@ -13,8 +13,8 @@ with open(path.join(here, 'README.rst'), encoding='utf-8') as f: setup( name='apkkit', - version='0.5', - description='Manage APK packages from Python', + version='0.6.0', + description='Manage APK packages and repositories from Python', long_description=long_description, url='http://adelielinux.org/', author='A. Wilcox', @@ -35,10 +35,11 @@ setup( 'Topic :: System :: Software Distribution', ], keywords='apk packaging portage', - packages=('apkkit',), + packages=find_packages(), install_requires=[ 'cryptography', 'jinja', 'pyyaml', + 'requests', ] ) -- cgit v1.2.3-60-g2f50