diff options
author | fuzzyray <fuzzyray@gentoo.org> | 2009-12-08 21:53:45 +0000 |
---|---|---|
committer | fuzzyray <fuzzyray@gentoo.org> | 2009-12-08 21:53:45 +0000 |
commit | acdf616efa73b77936963eaa8b5c715db97646d2 (patch) | |
tree | d08ef2efee8b7edbf8c1df1a8de26439d6b42bd3 /pym/gentoolkit/package.py | |
parent | Speedup portageq queries. Include FuzzyRay's patch to respect EMERGE_DEFAULT_... (diff) | |
download | gentoolkit-acdf616efa73b77936963eaa8b5c715db97646d2.tar.gz gentoolkit-acdf616efa73b77936963eaa8b5c715db97646d2.tar.bz2 gentoolkit-acdf616efa73b77936963eaa8b5c715db97646d2.zip |
Merge rev 113 from djanderson's genscripts repo
svn path=/trunk/gentoolkit/; revision=703
Diffstat (limited to 'pym/gentoolkit/package.py')
-rw-r--r-- | pym/gentoolkit/package.py | 666 |
1 files changed, 268 insertions, 398 deletions
diff --git a/pym/gentoolkit/package.py b/pym/gentoolkit/package.py index 857470a..e348258 100644 --- a/pym/gentoolkit/package.py +++ b/pym/gentoolkit/package.py @@ -7,159 +7,198 @@ # # $Header$ +"""Provides classes for accessing Portage db information for a given package.""" + +__all__ = ( + 'Package', + 'PackageFormatter' +) + # ======= -# Imports +# Imports # ======= import os import portage -from portage.versions import catpkgsplit, vercmp +from portage import settings import gentoolkit.pprinter as pp -from gentoolkit import settings, settingslock, PORTDB, VARDB from gentoolkit import errors -from gentoolkit.versionmatch import VersionMatch +from gentoolkit.cpv import CPV +from gentoolkit.dbapi import PORTDB, VARDB +from gentoolkit.dependencies import Dependencies +from gentoolkit.metadata import MetaData # ======= # Classes # ======= -class Package(object): - """Package descriptor. Contains convenience functions for querying the - state of a package, its contents, name manipulation, ebuild info and - similar.""" - - def __init__(self, arg): - - self._cpv = arg - self.cpv = self._cpv - - if self.cpv[0] in ('<', '>'): - if self.cpv[1] == '=': - self.operator = self.cpv[:2] - self.cpv = self.cpv[2:] - else: - self.operator = self.cpv[0] - self.cpv = self.cpv[1:] - elif self.cpv[0] == '=': - if self.cpv[-1] == '*': - self.operator = '=*' - self.cpv = self.cpv[1:-1] - else: - self.cpv = self.cpv[1:] - self.operator = '=' - elif self.cpv[0] == '~': - self.operator = '~' - self.cpv = self.cpv[1:] +class Package(CPV): + """Provides methods for ascertaining the state of a given CPV.""" + + def __init__(self, cpv): + if isinstance(cpv, CPV): + self.cpv = cpv else: - self.operator = '=' - self._cpv = '=%s' % self._cpv + self.cpv = CPV(cpv) + del cpv - if not portage.dep.isvalidatom(self._cpv): - raise errors.GentoolkitInvalidCPV(self._cpv) + if not all(getattr(self.cpv, x) for x in ('category', 'version')): + # CPV allows some things that Package must not + raise errors.GentoolkitInvalidPackage(str(self.cpv)) - cpv_split = portage.catpkgsplit(self.cpv) - - try: - self.key = "/".join(cpv_split[:2]) - except TypeError: - # catpkgsplit returned None - raise errors.GentoolkitInvalidCPV(self._cpv) - - cpv_split = list(cpv_split) - if cpv_split[0] == 'null': - cpv_split[0] = '' - if cpv_split[3] == 'r0': - cpv_split[3] = '' - self.cpv_split = cpv_split - self._scpv = self.cpv_split # XXX: namespace compatability 03/09 - - self._db = None - self._settings = settings - self._settingslock = settingslock - self._portdir_path = os.path.realpath(settings["PORTDIR"]) - - self.category = self.cpv_split[0] - self.name = self.cpv_split[1] - self.version = self.cpv_split[2] - self.revision = self.cpv_split[3] - if not self.revision: - self.fullversion = self.version - else: - self.fullversion = "%s-%s" % (self.version, self.revision) + # Set dynamically + self._package_path = None + self._dblink = None + self._metadata = None + self._deps = None + self._portdir_path = None def __repr__(self): - return "<%s %s @%#8x>" % (self.__class__.__name__, self._cpv, id(self)) + return "<%s %r>" % (self.__class__.__name__, str(self.cpv)) def __eq__(self, other): - return hash(self) == hash(other) + if not hasattr(other, 'cpv'): + return False + return self.cpv == other.cpv def __ne__(self, other): - return hash(self) != hash(other) + return not self == other def __lt__(self, other): - if not isinstance(other, self.__class__): - raise TypeError("other isn't of %s type, is %s" % - (self.__class__, other.__class__)) - - if self.category != other.category: - return self.category < other.category - elif self.name != other.name: - return self.name < other.name - else: - # FIXME: this cmp() hack is for vercmp not using -1,0,1 - # See bug 266493; this was fixed in portage-2.2_rc31 - #return portage.vercmp(self.fullversion, other.fullversion) - result = cmp(portage.vercmp(self.fullversion, other.fullversion), 0) - if result == -1: - return True - else: - return False + return self.cpv < other.cpv def __gt__(self, other): - return not self.__lt__(other) + return self.cpv > other.cpv def __hash__(self): - return hash(self._cpv) + return hash(str(self.cpv)) def __contains__(self, key): - return key in self._cpv - + return key in str(self.cpv) + def __str__(self): - return self._cpv + return str(self.cpv) + + def _get_trees(self): + """Return dbapi objects for each repository that contains self.""" + + result = [] + if self.is_installed(): + result.append(VARDB) + if self.exists(): + result.append(PORTDB) + if not result: + raise errors.GentoolkitFatalError("Could not find package tree") + + return result + + @property + def metadata(self): + """Instantiate a L{gentoolkit.metadata.MetaData} object here.""" + + if self._metadata is None: + metadata_path = os.path.join( + self.get_package_path(), 'metadata.xml' + ) + self._metadata = MetaData(metadata_path) + + return self._metadata + + @property + def dblink(self): + """Instantiate a L{portage.dbapi.vartree.dblink} object here.""" + + if self._dblink is None: + self._dblink = portage.dblink( + self.cpv.category, + "%s-%s" % (self.cpv.name, self.cpv.fullversion), + settings["ROOT"], + settings + ) + + return self._dblink + + @property + def deps(self): + """Instantiate a L{gentoolkit.dependencies.Dependencies} object here.""" + + if self._deps is None: + self._deps = Dependencies(self.cpv) - def get_name(self): - """Returns base name of package, no category nor version""" - return self.name + return self._deps - def get_version(self): - """Returns version of package, with revision number""" - return self.fullversion + def exists(self): + """Return True if package exists in the Portage tree, else False""" - def get_category(self): - """Returns category of package""" - return self.category + return bool(PORTDB.cpv_exists(str(self.cpv))) - def get_settings(self, key): - """Returns the value of the given key for this package (useful + @staticmethod + def get_settings(key): + """Returns the value of the given key for this package (useful for package.* files.""" + + if settings.locked: + settings.unlock() try: - self._settingslock.acquire() - self._settings.setcpv(self.cpv) - result = self._settings[key] + result = settings[key] finally: - self._settingslock.release() + settings.lock() + return result + + def get_mask_status(self): + """Shortcut to L{portage.getmaskingstatus}. + + @rtype: None or list + @return: a list containing none or some of: + 'profile' + 'package.mask' + license(s) + "kmask" keyword + 'missing keyword' + """ + + if settings.locked: + settings.unlock() + try: + result = portage.getmaskingstatus(str(self.cpv), + settings=settings, + portdb=PORTDB) + except KeyError: + # getmaskingstatus doesn't support packages without ebuilds in the + # Portage tree. + result = None + return result - def get_cpv(self): - """Returns full Category/Package-Version string""" - return self.cpv + def get_mask_reason(self): + """Shortcut to L{portage.getmaskingreason}. + + @rtype: None or tuple + @return: empty tuple if pkg not masked OR + ('mask reason', 'mask location') + """ + + try: + result = portage.getmaskingreason(str(self.cpv), + settings=settings, + PORTDB=PORTDB, + return_location=True) + if result is None: + result = tuple() + except KeyError: + # getmaskingstatus doesn't support packages without ebuilds in the + # Portage tree. + result = None + + return result def get_provide(self): """Return a list of provides, if any""" + if self.is_installed(): - result = VARDB.get_provide(self.cpv) + result = VARDB.get_provide(str(self.cpv)) else: try: result = [self.get_env_var('PROVIDE')] @@ -167,289 +206,114 @@ class Package(object): result = [] return result - def get_dependants(self): - """Retrieves a list of CPVs for all packages depending on this one""" - raise NotImplementedError("Not implemented yet!") + def get_ebuild_path(self, in_vartree=False): + """Returns the complete path to the .ebuild file. - def get_runtime_deps(self): - """Returns a linearised list of first-level run time dependencies for - this package, on the form [(comparator, [use flags], cpv), ...] + Example usage: + >>> pkg.get_ebuild_path() + '/usr/portage/sys-apps/portage/portage-2.1.6.13.ebuild' + >>> pkg.get_ebuild_path(in_vartree=True) + '/var/db/pkg/sys-apps/portage-2.1.6.13/portage-2.1.6.13.ebuild' """ - # Try to use the portage tree first, since emerge only uses the tree - # when calculating dependencies - try: - rdepends = self.get_env_var("RDEPEND", PORTDB).split() - except KeyError: - rdepends = self.get_env_var("RDEPEND", VARDB).split() - return self._parse_deps(rdepends)[0] - def get_compiletime_deps(self): - """Returns a linearised list of first-level compile time dependencies - for this package, on the form [(comparator, [use flags], cpv), ...] - """ - # Try to use the portage tree first, since emerge only uses the tree - # when calculating dependencies - try: - depends = self.get_env_var("DEPEND", PORTDB).split() - except KeyError: - depends = self.get_env_var("DEPEND", VARDB).split() - return self._parse_deps(depends)[0] + if in_vartree: + return VARDB.findname(str(self.cpv)) + return PORTDB.findname(str(self.cpv)) - def get_postmerge_deps(self): - """Returns a linearised list of first-level post merge dependencies - for this package, on the form [(comparator, [use flags], cpv), ...] - """ - # Try to use the portage tree first, since emerge only uses the tree - # when calculating dependencies - try: - postmerge_deps = self.get_env_var("PDEPEND", PORTDB).split() - except KeyError: - postmerge_deps = self.get_env_var("PDEPEND", VARDB).split() - return self._parse_deps(postmerge_deps)[0] + def get_package_path(self): + """Return the path to where the ebuilds and other files reside.""" - def intersects(self, other): - """Check if a passed in package atom "intersects" this atom. + if self._package_path is None: + path_split = self.get_ebuild_path().split(os.sep) + self._package_path = os.sep.join(path_split[:-1]) - Lifted from pkgcore. + return self._package_path - Two atoms "intersect" if a package can be constructed that - matches both: - - if you query for just "dev-lang/python" it "intersects" both - "dev-lang/python" and ">=dev-lang/python-2.4" - - if you query for "=dev-lang/python-2.4" it "intersects" - ">=dev-lang/python-2.4" and "dev-lang/python" but not - "<dev-lang/python-2.3" + def get_repo_name(self): + """Using the package path, determine the repo name. - @type other: L{gentoolkit.package.Package} - @param other: other package to compare - @see: pkgcore.ebuild.atom.py + @rtype: str + @return: /usr/<THIS>portage</THIS>/cat-egory/name/ """ - # Our "key" (cat/pkg) must match exactly: - if self.key != other.key: - return False - # If we are both "unbounded" in the same direction we intersect: - if (('<' in self.operator and '<' in other.operator) or - ('>' in self.operator and '>' in other.operator)): - return True - - # If one of us is an exact match we intersect if the other matches it: - if self.operator == '=': - if other.operator == '=*': - return self.fullversion.startswith(other.fullversion) - return VersionMatch(other).match(self) - if other.operator == '=': - if self.operator == '=*': - return other.fullversion.startswith(self.fullversion) - return VersionMatch(self).match(other) - - # If we are both ~ matches we match if we are identical: - if self.operator == other.operator == '~': - return (self.version == other.version and - self.revision == other.revision) - - # If we are both glob matches we match if one of us matches the other. - if self.operator == other.operator == '=*': - return (self.fullver.startswith(other.fullver) or - other.fullver.startswith(self.fullver)) - - # If one of us is a glob match and the other a ~ we match if the glob - # matches the ~ (ignoring a revision on the glob): - if self.operator == '=*' and other.operator == '~': - return other.fullversion.startswith(self.version) - if other.operator == '=*' and self.operator == '~': - return self.fullversion.startswith(other.version) - - # If we get here at least one of us is a <, <=, > or >=: - if self.operator in ('<', '<=', '>', '>='): - ranged, other = self, other - else: - ranged, other = other, self - - if '<' in other.operator or '>' in other.operator: - # We are both ranged, and in the opposite "direction" (or - # we would have matched above). We intersect if we both - # match the other's endpoint (just checking one endpoint - # is not enough, it would give a false positive on <=2 vs >2) - return ( - VersionMatch(other).match(ranged) and - VersionMatch(ranged).match(other)) - - if other.operator == '~': - # Other definitely matches its own version. If ranged also - # does we're done: - if VersionMatch(ranged).match(other): - return True - # The only other case where we intersect is if ranged is a - # > or >= on other's version and a nonzero revision. In - # that case other will match ranged. Be careful not to - # give a false positive for ~2 vs <2 here: - return ranged.operator in ('>', '>=') and VersionMatch( - other.operator, other.version, other.revision).match(ranged) - - if other.operator == '=*': - # a glob match definitely matches its own version, so if - # ranged does too we're done: - if VersionMatch( - ranged.operator, ranged.version, ranged.revision).match(other): - return True - if '<' in ranged.operator: - # If other.revision is not defined then other does not - # match anything smaller than its own fullver: - if not other.revision: - return False - - # If other.revision is defined then we can always - # construct a package smaller than other.fullver by - # tagging e.g. an _alpha1 on. - return ranged.fullversion.startswith(other.version) - else: - # Remaining cases where this intersects: there is a - # package greater than ranged.fullver and - # other.fullver that they both match. - return ranged.fullversion.startswith(other.version) - - # Handled all possible ops. - raise NotImplementedError( - 'Someone added an operator without adding it to intersects') - - - def _parse_deps(self,deps,curuse=[],level=0): - # store (comparator, [use predicates], cpv) - r = [] - comparators = ["~","<",">","=","<=",">="] - end = len(deps) - i = 0 - while i < end: - tok = deps[i] - if tok == ')': - return r,i - if tok[-1] == "?": - tok = tok.replace("?","") - sr,l = self._parse_deps(deps[i+2:],curuse=curuse+[tok],level=level+1) - r += sr - i += l + 3 - continue - if tok == "||": - sr,l = self._parse_deps(deps[i+2:],curuse,level=level+1) - r += sr - i += l + 3 - continue - # conjunction, like in "|| ( ( foo bar ) baz )" => recurse - if tok == "(": - sr,l = self._parse_deps(deps[i+1:],curuse,level=level+1) - r += sr - i += l + 2 - continue - # pkg block "!foo/bar" => ignore it - if tok[0] == "!": - i += 1 - continue - # pick out comparator, if any - cmp = "" - for c in comparators: - if tok.find(c) == 0: - cmp = c - tok = tok[len(cmp):] - r.append((cmp,curuse,tok)) - i += 1 - return r,i + return self.get_package_path().split(os.sep)[-3] - def is_installed(self): - """Returns True if this package is installed (merged)""" - return VARDB.cpv_exists(self.cpv) + def get_env_var(self, var, tree=None): + """Returns one of the predefined env vars DEPEND, SRC_URI, etc.""" - def is_overlay(self): - """Returns True if the package is in an overlay.""" - ebuild, tree = portage.portdb.findname2(self.cpv) - return tree != self._portdir_path + if tree is None: + tree = self._get_trees()[0] + try: + result = tree.aux_get(str(self.cpv), [var]) + if len(result) != 1: + raise errors.GentoolkitFatalError + except (KeyError, errors.GentoolkitFatalError): + err = "aux_get returned unexpected results" + raise errors.GentoolkitFatalError(err) + return result[0] - def is_masked(self): - """Returns true if this package is masked against installation. - Note: We blindly assume that the package actually exists on disk - somewhere.""" - unmasked = portage.portdb.xmatch("match-visible", self.cpv) - return self.cpv not in unmasked + def get_use_flags(self): + """Returns the USE flags active at time of installation.""" - def get_ebuild_path(self, in_vartree=False): - """Returns the complete path to the .ebuild file""" - if in_vartree: - return VARDB.getebuildpath(self.cpv) - return PORTDB.findname(self.cpv) + return self.dblink.getstring("USE") - def get_package_path(self): - """Returns the path to where the ChangeLog, Manifest, .ebuild files - reside""" - ebuild_path = self.get_ebuild_path() - path_split = ebuild_path.split("/") - if path_split: - return os.sep.join(path_split[:-1]) + def get_contents(self): + """Returns the parsed CONTENTS file. - def get_env_var(self, var, tree=None): - """Returns one of the predefined env vars DEPEND, RDEPEND, - SRC_URI,....""" - if tree == None: - tree = VARDB - if not self.is_installed(): - tree = PORTDB - result = tree.aux_get(self.cpv, [var]) - if not result: - raise errors.GentoolkitFatalError("Could not find the package tree") - if len(result) != 1: - raise errors.GentoolkitFatalError("Should only get one element!") - return result[0] + @rtype: dict + @return: {'/full/path/to/obj': ['type', 'timestamp', 'md5sum'], ...} + """ - def get_use_flags(self): - """Returns the USE flags active at time of installation""" - self._initdb() - if self.is_installed(): - return self._db.getfile("USE") + return self.dblink.getcontents() - def get_contents(self): - """Returns the full contents, as a dictionary, in the form - ['/bin/foo' : [ 'obj', '1052505381', '45ca8b89751...' ], ... ]""" - self._initdb() - if self.is_installed(): - return self._db.getcontents() - return {} + def get_size(self): + """Estimates the installed size of the contents of this package. - def size(self): - """Estimates the installed size of the contents of this package, - if possible. - Returns (size, number of files in total, number of uncounted files) + @rtype: tuple + @return: (size, number of files in total, number of uncounted files) """ + contents = self.get_contents() - size = 0 - uncounted = 0 - files = 0 - for x in contents: + size = n_uncounted = n_files = 0 + for cfile in contents: try: - size += os.lstat(x).st_size - files += 1 + size += os.lstat(cfile).st_size + n_files += 1 except OSError: - uncounted += 1 - return (size, files, uncounted) + n_uncounted += 1 + return (size, n_files, n_uncounted) - def _initdb(self): - """Internal helper function; loads package information from disk, - when necessary. - """ - if not self._db: - self._db = portage.dblink( - self.category, - "%s-%s" % (self.name, self.fullversion), - settings["ROOT"], - settings - ) + def is_installed(self): + """Returns True if this package is installed (merged)""" + + return VARDB.cpv_exists(str(self.cpv)) + + def is_overlay(self): + """Returns True if the package is in an overlay.""" + + ebuild, tree = PORTDB.findname2(str(self.cpv)) + if not ebuild: + return None + if self._portdir_path is None: + self._portdir_path = os.path.realpath(settings["PORTDIR"]) + return (tree and tree != self._portdir_path) + + def is_masked(self): + """Returns true if this package is masked against installation. + Note: We blindly assume that the package actually exists on disk + somewhere.""" + + unmasked = PORTDB.xmatch("match-visible", str(self.cpv)) + return str(self.cpv) not in unmasked class PackageFormatter(object): """When applied to a L{gentoolkit.package.Package} object, determine the location (Portage Tree vs. overlay), install status and masked status. That information can then be easily formatted and displayed. - + Example usage: - >>> from gentoolkit.helpers2 import find_packages + >>> from gentoolkit.helpers import find_packages >>> from gentoolkit.package import PackageFormatter >>> pkgs = [PackageFormatter(x) for x in find_packages('gcc')] >>> for pkg in pkgs: @@ -457,49 +321,45 @@ class PackageFormatter(object): ... # tree ... if set('IP').issubset(pkg.location): ... print pkg - ... + ... [IP-] [ ] sys-devel/gcc-4.3.2-r3 (4.3) @type pkg: L{gentoolkit.package.Package} @param pkg: package to format @type format: L{bool} - @param format: Whether to format the package name or not. + @param format: Whether to format the package name or not. Essentially C{format} should be set to False when piping or when - quiet output is desired. If C{format} is False, only the location + quiet output is desired. If C{do_format} is False, only the location attribute will be created to save time. """ - def __init__(self, pkg, format=True): - location = '' - maskmodes = [' ', ' ~', ' -', 'M ', 'M~', 'M-'] - + def __init__(self, pkg, do_format=True): self.pkg = pkg - self.format = format - if format: - self.arch = settings["ARCH"] - self.mask = maskmodes[self.get_mask_status()] - self.slot = pkg.get_env_var("SLOT") - self.location = self.get_package_location() + self.do_format = do_format + self.location = self.format_package_location() or '' def __repr__(self): return "<%s %s @%#8x>" % (self.__class__.__name__, self.pkg, id(self)) def __str__(self): - if self.format: + if self.do_format: + maskmodes = [' ', ' ~', ' -', 'M ', 'M~', 'M-', 'XX'] return "[%(location)s] [%(mask)s] %(package)s (%(slot)s)" % { 'location': self.location, - 'mask': pp.maskflag(self.mask), - 'package': pp.cpv(self.pkg.cpv), - 'slot': self.slot + 'mask': pp.maskflag(maskmodes[self.format_mask_status()[0]]), + 'package': pp.cpv(str(self.pkg.cpv)), + 'slot': self.pkg.get_env_var("SLOT") } else: return self.pkg.cpv - def get_package_location(self): - """Get the physical location of a package on disk. + def format_package_location(self): + """Get the install status (in /var/db/?) and origin (from and overlay + and the Portage tree?). @rtype: str @return: one of: + 'I--' : Installed but ebuild doesn't exist on system anymore '-P-' : Not installed and from the Portage tree '--O' : Not installed and from an overlay 'IP-' : Installed and from the Portage tree @@ -510,37 +370,47 @@ class PackageFormatter(object): if self.pkg.is_installed(): result[0] = 'I' - if self.pkg.is_overlay(): + + overlay = self.pkg.is_overlay() + if overlay is None: + pass + elif overlay: result[2] = 'O' else: result[1] = 'P' return ''.join(result) - def get_mask_status(self): - """Get the mask status of a given package. - - @type pkg: L{gentoolkit.package.Package} - @param pkg: pkg to get mask status of - @type arch: str - @param arch: output of gentoolkit.settings["ARCH"] - @rtype: int - @return: an index for this list: [" ", " ~", " -", "M ", "M~", "M-"] + def format_mask_status(self): + """Get the mask status of a given package. + + @rtype: tuple: (int, list) + @return: int = an index for this list: + [" ", " ~", " -", "M ", "M~", "M-", "XX"] 0 = not masked 1 = keyword masked 2 = arch masked 3 = hard masked 4 = hard and keyword masked, 5 = hard and arch masked + 6 = ebuild doesn't exist on system anymore + + list = original output of portage.getmaskingstatus """ - keywords = self.pkg.get_env_var("KEYWORDS").split() - mask_status = 0 - if self.pkg.is_masked(): - mask_status += 3 - if ("~%s" % self.arch) in keywords: - mask_status += 1 - elif ("-%s" % self.arch) in keywords or "-*" in keywords: - mask_status += 2 + result = 0 + masking_status = self.pkg.get_mask_status() + if masking_status is None: + return (6, []) + + if ("~%s keyword" % self.pkg.get_settings("ARCH")) in masking_status: + result += 1 + if "missing keyword" in masking_status: + result += 2 + if set(('profile', 'package.mask')).intersection(masking_status): + result += 3 + + return (result, masking_status) + - return mask_status +# vim: set ts=4 sw=4 tw=79: |