Verified Commit 2a46aec4 authored by A. Wilcox's avatar A. Wilcox 🦊
Browse files

Add APKINDEX reading support

parent 20d58e4a
"""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)
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)
def description(self):
"""The description of this repository."""
return self._desc
def packages(self):
"""The packages available in this repository."""
return list(self._pkgs)
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.
tar =
real_buf = tar.extractfile('APKINDEX')
real_buf = buf
for line in real_buf.readlines():
if not isinstance(line, str):
line = line.decode('utf-8')
# Skip comments.
if line[0] == '#':
# separated by blank line
if line == "\n" and params.get('name', None) is not None:
if line.find(':') == -1:
INDEX_LOGGER.warning('!!! malformed line? "%s" !!!', line)
(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(' ')
params[param_map[key]] = value
else:'!!! unrecognised APKINDEX key %s !!!', key)
self._pkgs = pkgs
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).
A :py:class:`Index` object with the details from the APKINDEX.
idx = cls(packages=list())
return idx
......@@ -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__)
# Generated by APK Kit for Adélie Linux
# {{ builduser }}@{{ buildhost }} {{ builddate }}
# {{ builduser }}@{{ buildhost }} {{ self.builddate }}
pkgname = {{ }}
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
def replaces(self):
"""The packages this package replaces."""
return self._replaces
def install_if(self):
"""The packages that pull in this package."""
return self._iif
def license(self):
"""The license of the package."""
return self._license
def origin(self):
"""The origin package of this package."""
return self._origin
def commit(self):
"""The hash of the git commit the build repository was on."""
return self._commit
def maintainer(self):
"""The maintainer of the package."""
return self._maintainer
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):
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:
description='Manage APK packages from Python',
description='Manage APK packages and repositories from Python',
author='A. Wilcox',
......@@ -35,10 +35,11 @@ setup(
'Topic :: System :: Software Distribution',
keywords='apk packaging portage',
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment