# Copyright(c) 2004, Karl Trygve Kalleberg # Copyright(c) 2004-2024, Gentoo Authors # # Licensed under the GNU General Public License, v2 """Provides an interface to package information stored by package managers. The Package class is the heart of much of Gentoolkit. Given a CPV (category/package-version) string, it can reveal the package's status in the tree and VARDB (/var/db/), provide rich comparison and sorting, and expose important parts of Portage's back-end. Example usage: >>> portage = Package('sys-apps/portage-9999') >>> portage.ebuild_path() '/var/db/repos/gentoo/sys-apps/portage/portage-9999.ebuild' >>> portage.is_masked() True >>> portage.is_installed() False """ __all__ = ("Package", "PackageFormatter", "FORMAT_TMPL_VARS") # ======= # Globals # ======= FORMAT_TMPL_VARS = ( "$location", "$mask", "$mask2", "$cp", "$cpv", "$category", "$name", "$version", "$revision", "$fullversion", "$slot", "$repo", "$keywords", ) # ======= # Imports # ======= import os from string import Template import portage from portage.util import LazyItemsDict from portage import _encodings, _unicode_encode import gentoolkit.pprinter as pp from gentoolkit import errors from gentoolkit.cpv import CPV from gentoolkit.keyword import determine_keyword from gentoolkit.flag import get_flags from gentoolkit.eprefix import EPREFIX # ======= # Settings # ======= def _NewPortageConfig(local_config): ret = portage.config( local_config=local_config, eprefix=EPREFIX if EPREFIX else None, config_root=os.environ.get("PORTAGE_CONFIGROOT", None), target_root=os.environ.get("ROOT", None), ) ret.lock() return ret default_settings = _NewPortageConfig(local_config=True) nolocal_settings = _NewPortageConfig(local_config=False) # ======= # Classes # ======= class Package(CPV): """Exposes the state of a given CPV.""" def __init__(self, cpv, validate=False, local_config=True): if isinstance(cpv, CPV): self.__dict__.update(cpv.__dict__) else: CPV.__init__(self, cpv, validate=validate) if validate and not all(hasattr(self, x) for x in ("category", "version")): # CPV allows some things that Package must not raise errors.GentoolkitInvalidPackage(self.cpv) if local_config: self._settings = default_settings else: self._settings = nolocal_settings # Set dynamically self._package_path = None self._dblink = None self._metadata = None self._deps = None self._portdir_path = None def __repr__(self): return f"<{self.__class__.__name__} {self.cpv!r}>" def __hash__(self): return hash(self.cpv) def __contains__(self, key): return key in self.cpv def __str__(self): return str(self.cpv) @property def metadata(self): """Instantiate a L{portage.xml.MetaDataXML} object here.""" from portage.xml.metadata import MetaDataXML if self._metadata is None: metadata_path = os.path.join(self.package_path(), "metadata.xml") projects_path = os.path.join( self._settings["PORTDIR"], "metadata/projects.xml" ) try: self._metadata = MetaDataXML(metadata_path, projects_path) except OSError as error: import errno if error.errno != errno.ENOENT: raise return None 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.category, f"{self.name}-{self.fullversion}", self._settings["ROOT"], self._settings, ) return self._dblink @property def deps(self): """Instantiate a L{gentoolkit.dependencies.Dependencies} object here.""" from gentoolkit.dependencies import Dependencies if self._deps is None: self._deps = Dependencies(self.cpv) return self._deps def environment(self, envvars, prefer_vdb=True, fallback=True): """Returns one or more of the predefined environment variables. Some available envvars are: ---------------------- BINPKGMD5 COUNTER FEATURES LICENSE SRC_URI CATEGORY CXXFLAGS HOMEPAGE PDEPEND USE CBUILD DEFINED_PHASES INHERITED PF CFLAGS DEPEND IUSE PROVIDE CHOST DESCRIPTION KEYWORDS RDEPEND CONTENTS EAPI LDFLAGS SLOT Example usage: >>> pkg = Package('sys-apps/portage-9999') >>> pkg.environment('USE') '' >>> pkg.environment(('USE', 'IUSE')) ... # doctest: +NORMALIZE_WHITESPACE ['', 'build doc epydoc +ipc pypy1_9 python2 python3 selinux xattr'] @type envvars: str or array @param envvars: one or more of (DEPEND, SRC_URI, etc.) @type prefer_vdb: bool @keyword prefer_vdb: if True, look in the vardb before portdb, else reverse order. Specifically KEYWORDS will get more recent information by preferring portdb. @type fallback: bool @keyword fallback: query only the preferred db if False @rtype: str or list @return: str if envvars is str, list if envvars is array @raise KeyError: if key is not found in requested db(s) """ got_string = False if isinstance(envvars, str): got_string = True envvars = (envvars,) if prefer_vdb: try: result = portage.db[portage.root]["vartree"].dbapi.aux_get( self.cpv, envvars ) except KeyError: try: if not fallback: raise KeyError result = portage.db[portage.root]["porttree"].dbapi.aux_get( self.cpv, envvars ) except KeyError: raise errors.GentoolkitFatalError( "aux_get returned unexpected " "results" ) else: try: result = portage.db[portage.root]["porttree"].dbapi.aux_get( self.cpv, envvars ) except KeyError: try: if not fallback: raise KeyError result = portage.db[portage.root]["vartree"].dbapi.aux_get( self.cpv, envvars ) except KeyError: raise errors.GentoolkitFatalError( "aux_get returned unexpected " "results" ) if got_string: return result[0] return result def exists(self): """Return True if package exists in the Portage tree, else False""" return bool(portage.db[portage.root]["porttree"].dbapi.cpv_exists(self.cpv)) def settings(self, key): """Returns the value of the given key for this package (useful for package.* files.""" if self._settings.locked: self._settings.unlock() try: result = self._settings[key] finally: self._settings.lock() return result def 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 self._settings.locked: self._settings.unlock() try: result = portage.getmaskingstatus( self.cpv, settings=self._settings, portdb=portage.db[portage.root]["porttree"].dbapi, ) except KeyError: # getmaskingstatus doesn't support packages without ebuilds in the # Portage tree. result = None return result def 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( self.cpv, settings=self._settings, portdb=portage.db[portage.root]["porttree"].dbapi, 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 ebuild_path(self, in_vartree=False): """Returns the complete path to the .ebuild file. Example usage: >>> pkg = Package('sys-apps/portage-9999') >>> pkg.ebuild_path() '/var/db/repos/gentoo/sys-apps/portage/portage-9999.ebuild' >>> pkg.ebuild_path(in_vartree=True) '/var/db/pkg/sys-apps/portage-9999/portage-9999.ebuild' """ if in_vartree: return portage.db[portage.root]["vartree"].dbapi.findname(self.cpv) return portage.db[portage.root]["porttree"].dbapi.findname(self.cpv) def package_path(self, in_vartree=False): """Return the path to where the ebuilds and other files reside.""" if in_vartree: return self.dblink.getpath() return os.sep.join(self.ebuild_path().split(os.sep)[:-1]) def repo_name(self, fallback=True): """Determine the repository name. @type fallback: bool @param fallback: if the repo_name file does not exist, return the repository name from the path @rtype: str @return: output of the repository metadata file, which stores the repo_name variable, or try to get the name of the repo from the path. @raise GentoolkitFatalError: if fallback is False and repo_name is not specified by the repository. """ try: return self.environment("repository") except errors.GentoolkitFatalError: if fallback: return self.package_path().split(os.sep)[-3] raise def use(self): """Returns the USE flags active at time of installation.""" return self.dblink.getstring("USE") def use_status(self): """Returns the USE flags active for installation.""" iuse, final_flags = get_flags(self.cpv, final_setting=True) return final_flags def parsed_contents(self, prefix_root=False): """Returns the parsed CONTENTS file. @rtype: dict @return: {'/full/path/to/obj': ['type', 'timestamp', 'md5sum'], ...} """ contents = self.dblink.getcontents() # Portage will automatically prepend ROOT. Undo that. if not prefix_root: myroot = self._settings["ROOT"] if myroot != "/": ret = {} for key, val in self.dblink.getcontents().items(): ret["/" + os.path.relpath(key, myroot)] = val contents = ret return contents def size(self): """Estimates the installed size of the contents of this package. @rtype: tuple @return: (size, number of files in total, number of uncounted files) """ seen = set() size = n_files = n_uncounted = 0 for path in self.parsed_contents(prefix_root=True): try: st = os.lstat(_unicode_encode(path, encoding=_encodings["fs"])) except OSError: continue # Remove hardlinks by checking for duplicate inodes. Bug #301026. file_inode = st.st_ino if file_inode in seen: continue seen.add(file_inode) try: size += st.st_size n_files += 1 except OSError: n_uncounted += 1 return (size, n_files, n_uncounted) def is_installed(self): """Returns True if this package is installed (merged).""" return self.dblink.exists() def is_overlay(self): """Returns True if the package is in an overlay.""" ebuild, tree = portage.db[portage.root]["porttree"].dbapi.findname2(self.cpv) if not ebuild: return None if self._portdir_path is None: self._portdir_path = os.path.realpath(self._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. """ unmasked = portage.db[portage.root]["porttree"].dbapi.xmatch( "match-visible", self.cpv ) return self.cpv not in unmasked @property def description(self): """Returns the DESCRIPTION from the ebuild @rtype: list """ return portage.db[portage.root]["porttree"].dbapi.aux_get( self.cpv, ["DESCRIPTION"] ) class PackageFormatter: """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.query import Query >>> from gentoolkit.package import PackageFormatter >>> import portage.output >>> q = Query('gcc') >>> pkgs = [PackageFormatter(x) for x in q.find()] >>> havecolor = portage.output.havecolor >>> portage.output.havecolor = False >>> for pkg in pkgs: ... # Only print packages that are installed and from the Portage ... # tree ... if set('IP').issubset(pkg.location): ... print(pkg) ... [IP-] [ ] sys-devel/gcc-4.5.4:4.5 >>> portage.output.havecolor = havecolor @type pkg: L{gentoolkit.package.Package} @param pkg: package to format @type do_format: bool @param do_format: Whether to format the package name or not. Essentially C{do_format} should be set to False when piping or when quiet output is desired. If C{do_format} is False, only the location attribute will be created to save time. """ _tmpl_verbose = "[$location] [$mask] $cpv:$slot" _tmpl_quiet = "$cpv" def __init__(self, pkg, do_format=True, custom_format=None): self._pkg = None self._do_format = do_format self._str = None self._location = None if not custom_format: if do_format: custom_format = self._tmpl_verbose else: custom_format = self._tmpl_quiet self.tmpl = Template(custom_format) self.format_vars = LazyItemsDict() self.pkg = pkg def __repr__(self): return f"<{self.__class__.__name__} {self.pkg} @{id(self):#8x}>" def __str__(self): if self._str is None: self._str = self.tmpl.safe_substitute(self.format_vars) return self._str @property def location(self): if self._location is None: self._location = self.format_package_location() return self._location @property def pkg(self): """Package to format""" return self._pkg @pkg.setter def pkg(self, value): if self._pkg == value: return self._pkg = value self._location = None fmt_vars = self.format_vars self.format_vars.clear() fmt_vars.addLazySingleton("location", lambda: getattr(self, "location")) fmt_vars.addLazySingleton("mask", self.format_mask) fmt_vars.addLazySingleton("mask2", self.format_mask_status2) fmt_vars.addLazySingleton("cpv", self.format_cpv) fmt_vars.addLazySingleton("cp", self.format_cpv, "cp") fmt_vars.addLazySingleton("category", self.format_cpv, "category") fmt_vars.addLazySingleton("name", self.format_cpv, "name") fmt_vars.addLazySingleton("version", self.format_cpv, "version") fmt_vars.addLazySingleton("revision", self.format_cpv, "revision") fmt_vars.addLazySingleton("fullversion", self.format_cpv, "fullversion") fmt_vars.addLazySingleton("slot", self.format_slot) fmt_vars.addLazySingleton("repo", self.pkg.repo_name) fmt_vars.addLazySingleton("keywords", self.format_keywords) def format_package_location(self): """Get the install status (in /var/db/?) and origin (from an 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 'I-O' : Installed and from an overlay """ result = ["-", "-", "-"] if self.pkg.is_installed(): result[0] = "I" overlay = self.pkg.is_overlay() if overlay is None: pass elif overlay: result[2] = "O" else: result[1] = "P" return "".join(result) 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-", "??"] 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 """ result = 0 masking_status = self.pkg.mask_status() if masking_status is None: return (6, []) if ("~%s keyword" % self.pkg.settings("ARCH")) in masking_status: result += 1 if "missing keyword" in masking_status: result += 2 if {"profile", "package.mask"}.intersection(masking_status): result += 3 return (result, masking_status) def format_mask_status2(self): """Get the mask status of a given package.""" mask = self.pkg.mask_status() if mask: return pp.masking(mask) else: arch = self.pkg.settings("ARCH") keywords = self.pkg.environment("KEYWORDS") mask = [ determine_keyword(arch, portage.settings["ACCEPT_KEYWORDS"], keywords) ] return pp.masking(mask) def format_mask(self): maskmodes = [" ", " ~", " -", "M ", "M~", "M-", "??"] maskmode = maskmodes[self.format_mask_status()[0]] return pp.keyword( maskmode, stable=not maskmode.strip(), hard_masked={"M", "?", "-"}.intersection(maskmode), ) def format_cpv(self, attr=None): if attr is None: value = self.pkg.cpv else: value = getattr(self.pkg, attr) if self._do_format: return pp.cpv(value) else: return value def format_slot(self): value = self.pkg.environment("SLOT") if self._do_format: return pp.slot(value) else: return value def format_keywords(self): value = self.pkg.environment("KEYWORDS") if self._do_format: return pp.keyword(value) else: return value # vim: set ts=4 sw=4 tw=79: