blob: d23123945de005a5a7a90c046b489354ee456701 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Package Info (CPV) parsing."""
from __future__ import print_function
import collections
import functools
import re
import string
from typing import Union
# Define data structures for holding PV and CPV objects.
_PV_FIELDS = ['pv', 'package', 'version', 'version_no_rev', 'rev']
PV = collections.namedtuple('PV', _PV_FIELDS)
# See ebuild(5) man page for the field specs these fields are based on.
# Notably, cpv does not include the revision, cpf does.
_CPV_FIELDS = ['category', 'cp', 'cpv', 'cpf'] + _PV_FIELDS
CPV = collections.namedtuple('CPV', _CPV_FIELDS)
# Package matching regexp, as dictated by package manager specification:
# https://www.gentoo.org/proj/en/qa/pms.xml
_pkg = r'(?P<package>' + r'[\w+][\w+-]*)'
_ver = (r'(?P<version>'
r'(?P<version_no_rev>(\d+)((\.\d+)*)([a-z]?)'
r'((_(pre|p|beta|alpha|rc)\d*)*))'
r'(-(?P<rev>r(\d+)))?)')
_pvr_re = re.compile(r'^(?P<pv>%s-%s)$' % (_pkg, _ver), re.VERBOSE)
def _SplitPV(pv, strict=True):
"""Takes a PV value and splits it into individual components.
Deprecated, use parse() instead.
Args:
pv: Package name and version.
strict: If True, returns None if version or package name is missing.
Otherwise, only package name is mandatory.
Returns:
A collection with named members:
pv, package, version, version_no_rev, rev
"""
m = _pvr_re.match(pv)
if m is None and strict:
return None
if m is None:
return PV(**{'pv': None, 'package': pv, 'version': None,
'version_no_rev': None, 'rev': None})
return PV(**m.groupdict())
def SplitCPV(cpv, strict=True):
"""Splits a CPV value into components.
Deprecated, use parse() instead.
Args:
cpv: Category, package name, and version of a package.
strict: If True, returns None if any of the components is missing.
Otherwise, only package name is mandatory.
Returns:
A collection with named members:
category, pv, package, version, version_no_rev, rev
"""
chunks = cpv.split('/')
if len(chunks) > 2:
raise ValueError('Unexpected package format %s' % cpv)
if len(chunks) == 1:
category = None
else:
category = chunks[0]
m = _SplitPV(chunks[-1], strict=strict)
if strict and (category is None or m is None):
return None
# Gather parts and build each field. See ebuild(5) man page for spec.
cp_fields = (category, m.package)
cp = '%s/%s' % cp_fields if all(cp_fields) else None
cpv_fields = (cp, m.version_no_rev)
real_cpv = '%s-%s' % cpv_fields if all(cpv_fields) else None
cpf_fields = (real_cpv, m.rev)
cpf = '%s-%s' % cpf_fields if all(cpf_fields) else real_cpv
return CPV(category=category, cp=cp, cpv=real_cpv, cpf=cpf, **m._asdict())
def parse(cpv: Union[str, CPV, 'PackageInfo']):
"""Parse a package to a PackageInfo object.
Args:
cpv: Any package type. This function can parse strings, translate CPVs to a
PackageInfo instance, and will simply return the argument if given a
PackageInfo instance.
Returns:
PackageInfo
"""
if isinstance(cpv, PackageInfo):
return cpv
elif isinstance(cpv, CPV):
parsed = cpv
else:
parsed = SplitCPV(cpv, strict=False)
# Temporary measure. SplitCPV parses X-r1 with the revision as r1.
# Once the SplitCPV function has been fully deprecated we can switch
# the regex to exclude the r from what it parses as the revision instead.
# TODO: Change the regex to parse the revision without the r.
revision = parsed.rev.replace('r', '') if parsed.rev else None
return PackageInfo(
category=parsed.category,
package=parsed.package,
version=parsed.version_no_rev,
revision=revision)
class PackageInfo(object):
"""Read-only class to hold and format commonly used package information."""
def __init__(self, category=None, package=None, version=None, revision=None):
# Private attributes to enforce read-only. Particularly to allow use of
# lru_cache for formatting.
self._category = category
self._package = package
self._version = version
self._revision = int(revision) if revision else 0
def __eq__(self, other):
try:
return (self.category == other.category and
self.package == other.package and
str(self.version) == str(other.version) and
str(self.revision) == str(other.revision))
except AttributeError:
return False
def __hash__(self):
return hash((self._category, self._package, self._version, self._revision))
@functools.lru_cache()
def __format__(self, format_spec):
"""Formatter function.
The format |spec| is a format string containing any combination of:
{c}, {p}, {v}, or {r} for the package's category, package name, version,
or revision, respectively, or any of the class' {attribute}s.
e.g. {c}/{p} or {atom} for a package's atom (i.e. category/package_name).
"""
fmtter = string.Formatter()
base_dict = {
'c': self.category,
'p': self.package,
'v': self.version,
# Force 'r' to be None when we have 0 to avoid -r0 suffixes.
'r': self.revision or None,
}
fields = (x for _, x, _, _ in fmtter.parse(format_spec) if x)
# Setting base_dict.get(x) as the default value for getattr allows it to
# fall back to valid, falsey values in the base_dict rather than
# overwriting them with None, i.e. 0 for version or revision.
fmt_dict = {x: getattr(self, x, base_dict.get(x)) for x in fields}
# We can almost do `if all(fmt_dict.values()):` to just check for falsey
# values here, but 0 is a valid version value.
if any(v in ('', None) for v in fmt_dict.values()):
return ''
return format_spec.format(**fmt_dict)
@property
def category(self):
return self._category
@property
def package(self):
return self._package
@property
def version(self):
return self._version
@property
def revision(self):
return self._revision
@property
def cpv(self):
return format(self, '{c}/{p}-{v}')
@property
def cpvr(self):
return format(self, '{cpv}-r{r}') or self.cpv
@property
def cpf(self):
"""CPF is the portage name for cpvr, provided to simplify transition."""
return self.cpvr
@property
def atom(self):
return format(self, '{c}/{p}')
@property
def cp(self):
return self.atom
@property
def pv(self):
return format(self, '{p}-{v}')
@property
def pvr(self):
return format(self, '{pv}-r{r}') or self.pv
@property
def vr(self):
return format(self, '{v}-r{r}') or self.version
@property
def ebuild(self):
return format(self, '{pvr}.ebuild')
@property
def relative_path(self):
"""Path of the ebuild relative to its overlay."""
return format(self, '{c}/{p}/{ebuild}')
def revision_bump(self):
"""Get a PackageInfo instance with an incremented revision."""
return PackageInfo(self.category, self.package, self.version,
self.revision + 1)
def with_version(self, version):
"""Get a PackageInfo instance with the new, specified version."""
return PackageInfo(self.category, self.package, version)