| # -*- 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)) |
| |
| def __repr__(self): |
| return f'PackageInfo<{str(self)}>' |
| |
| def __str__(self): |
| return self.cpvr or self.atom |
| |
| @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) |
| |
| def to_cpv(self): |
| """Get a CPV instance of this PackageInfo. |
| |
| This method is provided only to allow compatibility with functions that |
| have not yet been converted to use PackageInfo objects. This function will |
| be removed when the CPV namedtuple is removed. |
| """ |
| return SplitCPV(self.cpvr) |