| # Copyright 1999-2020 Gentoo Authors |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import warnings |
| |
| from _emerge.Package import Package |
| from _emerge.PackageVirtualDbapi import PackageVirtualDbapi |
| from _emerge.resolver.DbapiProvidesIndex import PackageDbapiProvidesIndex |
| |
| import portage |
| from portage import os |
| from portage.const import VDB_PATH |
| from portage.dbapi.vartree import vartree |
| from portage.dep._slot_operator import find_built_slot_operator_atoms |
| from portage.eapi import _get_eapi_attrs |
| from portage.exception import InvalidData, InvalidDependString |
| from portage.update import grab_updates, parse_updates, update_dbentries |
| from portage.versions import _pkg_str |
| |
| |
| class FakeVardbGetPath: |
| """ |
| Implements the vardbapi.getpath() method which is used in error handling |
| code for the Package class and vartree.get_provide(). |
| """ |
| |
| def __init__(self, vardb): |
| self.settings = vardb.settings |
| |
| def __call__(self, cpv, filename=None): |
| path = os.path.join(self.settings["EROOT"], VDB_PATH, cpv) |
| if filename is not None: |
| path = os.path.join(path, filename) |
| return path |
| |
| |
| class _DynamicDepsNotApplicable(Exception): |
| pass |
| |
| |
| class FakeVartree(vartree): |
| """This is implements an in-memory copy of a vartree instance that provides |
| all the interfaces required for use by the depgraph. The vardb is locked |
| during the constructor call just long enough to read a copy of the |
| installed package information. This allows the depgraph to do it's |
| dependency calculations without holding a lock on the vardb. It also |
| allows things like vardb global updates to be done in memory so that the |
| user doesn't necessarily need write access to the vardb in cases where |
| global updates are necessary (updates are performed when necessary if there |
| is not a matching ebuild in the tree). Instances of this class are not |
| populated until the sync() method is called.""" |
| |
| def __init__( |
| self, |
| root_config, |
| pkg_cache=None, |
| pkg_root_config=None, |
| dynamic_deps=False, |
| ignore_built_slot_operator_deps=False, |
| soname_deps=False, |
| ): |
| self._root_config = root_config |
| self._dynamic_deps = dynamic_deps |
| self._ignore_built_slot_operator_deps = ignore_built_slot_operator_deps |
| if pkg_root_config is None: |
| pkg_root_config = self._root_config |
| self._pkg_root_config = pkg_root_config |
| if pkg_cache is None: |
| pkg_cache = {} |
| real_vartree = root_config.trees["vartree"] |
| self._real_vardb = real_vartree.dbapi |
| portdb = root_config.trees["porttree"].dbapi |
| self.settings = real_vartree.settings |
| mykeys = list(real_vartree.dbapi._aux_cache_keys) |
| if "_mtime_" not in mykeys: |
| mykeys.append("_mtime_") |
| self._db_keys = mykeys |
| self._pkg_cache = pkg_cache |
| self.dbapi = PackageVirtualDbapi(real_vartree.settings) |
| if soname_deps: |
| self.dbapi = PackageDbapiProvidesIndex(self.dbapi) |
| self.dbapi.getpath = FakeVardbGetPath(self.dbapi) |
| self.dbapi._aux_cache_keys = set(self._db_keys) |
| |
| # Initialize variables needed for lazy cache pulls of the live ebuild |
| # metadata. This ensures that the vardb lock is released ASAP, without |
| # being delayed in case cache generation is triggered. |
| self._aux_get = self.dbapi.aux_get |
| self._match = self.dbapi.match |
| if dynamic_deps: |
| self.dbapi.aux_get = self._aux_get_wrapper |
| self.dbapi.match = self._match_wrapper |
| self._aux_get_history = set() |
| self._portdb_keys = Package._dep_keys + ("EAPI", "KEYWORDS") |
| self._portdb = portdb |
| self._global_updates = None |
| |
| @property |
| def root(self): |
| warnings.warn( |
| "The root attribute of " |
| "_emerge.FakeVartree.FakeVartree" |
| " is deprecated. Use " |
| "settings['ROOT'] instead.", |
| DeprecationWarning, |
| stacklevel=3, |
| ) |
| return self.settings["ROOT"] |
| |
| def _match_wrapper(self, cpv, use_cache=1): |
| """ |
| Make sure the metadata in Package instances gets updated for any |
| cpv that is returned from a match() call, since the metadata can |
| be accessed directly from the Package instance instead of via |
| aux_get(). |
| """ |
| matches = self._match(cpv, use_cache=use_cache) |
| for cpv in matches: |
| if cpv in self._aux_get_history: |
| continue |
| self._aux_get_wrapper(cpv, []) |
| return matches |
| |
| def _aux_get_wrapper(self, cpv, wants, myrepo=None): |
| if cpv in self._aux_get_history: |
| return self._aux_get(cpv, wants) |
| self._aux_get_history.add(cpv) |
| |
| # This raises a KeyError to the caller if appropriate. |
| pkg = self.dbapi._cpv_map[cpv] |
| |
| try: |
| live_metadata = dict( |
| zip( |
| self._portdb_keys, |
| self._portdb.aux_get(cpv, self._portdb_keys, myrepo=pkg.repo), |
| ) |
| ) |
| except (KeyError, portage.exception.PortageException): |
| live_metadata = None |
| |
| self._apply_dynamic_deps(pkg, live_metadata) |
| |
| return self._aux_get(cpv, wants) |
| |
| def _apply_dynamic_deps(self, pkg, live_metadata): |
| |
| try: |
| if live_metadata is None: |
| raise _DynamicDepsNotApplicable() |
| # Use the metadata from the installed instance if the EAPI |
| # of either instance is unsupported, since if the installed |
| # instance has an unsupported or corrupt EAPI then we don't |
| # want to attempt to do complex operations such as execute |
| # pkg_config, pkg_prerm or pkg_postrm phases. If both EAPIs |
| # are supported then go ahead and use the live_metadata, in |
| # order to respect dep updates without revision bump or EAPI |
| # bump, as in bug #368725. |
| if not ( |
| portage.eapi_is_supported(live_metadata["EAPI"]) |
| and portage.eapi_is_supported(pkg.eapi) |
| ): |
| raise _DynamicDepsNotApplicable() |
| |
| # preserve built slot/sub-slot := operator deps |
| built_slot_operator_atoms = None |
| if ( |
| not self._ignore_built_slot_operator_deps |
| and _get_eapi_attrs(pkg.eapi).slot_operator |
| ): |
| try: |
| built_slot_operator_atoms = find_built_slot_operator_atoms(pkg) |
| except InvalidDependString: |
| pass |
| |
| if built_slot_operator_atoms: |
| live_eapi_attrs = _get_eapi_attrs(live_metadata["EAPI"]) |
| if not live_eapi_attrs.slot_operator: |
| raise _DynamicDepsNotApplicable() |
| for k, v in built_slot_operator_atoms.items(): |
| live_metadata[k] += " " + " ".join(str(atom) for atom in v) |
| |
| self.dbapi.aux_update(pkg.cpv, live_metadata) |
| except _DynamicDepsNotApplicable: |
| if self._global_updates is None: |
| self._global_updates = grab_global_updates(self._portdb) |
| |
| # Bypass _aux_get_wrapper, since calling that |
| # here would trigger infinite recursion. |
| aux_keys = Package._dep_keys + self.dbapi._pkg_str_aux_keys |
| aux_dict = dict(zip(aux_keys, self._aux_get(pkg.cpv, aux_keys))) |
| perform_global_updates(pkg.cpv, aux_dict, self.dbapi, self._global_updates) |
| |
| def dynamic_deps_preload(self, pkg, metadata): |
| if metadata is not None: |
| metadata = dict((k, metadata.get(k, "")) for k in self._portdb_keys) |
| self._apply_dynamic_deps(pkg, metadata) |
| self._aux_get_history.add(pkg.cpv) |
| |
| def cpv_discard(self, pkg): |
| """ |
| Discard a package from the fake vardb if it exists. |
| """ |
| old_pkg = self.dbapi.get(pkg) |
| if old_pkg is not None: |
| self.dbapi.cpv_remove(old_pkg) |
| self._pkg_cache.pop(old_pkg, None) |
| self._aux_get_history.discard(old_pkg.cpv) |
| |
| def sync(self, acquire_lock=1): |
| """ |
| Call this method to synchronize state with the real vardb |
| after one or more packages may have been installed or |
| uninstalled. |
| """ |
| locked = False |
| try: |
| if acquire_lock and os.access(self._real_vardb._dbroot, os.W_OK): |
| self._real_vardb.lock() |
| locked = True |
| self._sync() |
| finally: |
| if locked: |
| self._real_vardb.unlock() |
| |
| # Populate the old-style virtuals using the cached values. |
| # Skip the aux_get wrapper here, to avoid unwanted |
| # cache generation. |
| try: |
| self.dbapi.aux_get = self._aux_get |
| self.settings._populate_treeVirtuals_if_needed(self) |
| finally: |
| if self._dynamic_deps: |
| self.dbapi.aux_get = self._aux_get_wrapper |
| |
| def _sync(self): |
| |
| real_vardb = self._root_config.trees["vartree"].dbapi |
| current_cpv_set = frozenset(real_vardb.cpv_all()) |
| pkg_vardb = self.dbapi |
| |
| # Remove any packages that have been uninstalled. |
| for pkg in list(pkg_vardb): |
| if pkg.cpv not in current_cpv_set: |
| self.cpv_discard(pkg) |
| |
| # Validate counters and timestamps. |
| slot_counters = {} |
| root_config = self._pkg_root_config |
| validation_keys = ["COUNTER", "_mtime_"] |
| for cpv in current_cpv_set: |
| |
| pkg_hash_key = Package._gen_hash_key( |
| cpv=cpv, installed=True, root_config=root_config, type_name="installed" |
| ) |
| pkg = pkg_vardb.get(pkg_hash_key) |
| if pkg is not None: |
| counter, mtime = real_vardb.aux_get(cpv, validation_keys) |
| try: |
| counter = int(counter) |
| except ValueError: |
| counter = 0 |
| |
| if counter != pkg.counter or mtime != pkg.mtime: |
| self.cpv_discard(pkg) |
| pkg = None |
| |
| if pkg is None: |
| pkg = self._pkg(cpv) |
| |
| other_counter = slot_counters.get(pkg.slot_atom) |
| if other_counter is not None: |
| if other_counter > pkg.counter: |
| continue |
| |
| slot_counters[pkg.slot_atom] = pkg.counter |
| pkg_vardb.cpv_inject(pkg) |
| |
| real_vardb.flush_cache() |
| |
| def _pkg(self, cpv): |
| """ |
| The RootConfig instance that will become the Package.root_config |
| attribute can be overridden by the FakeVartree pkg_root_config |
| constructory argument, since we want to be consistent with the |
| depgraph._pkg() method which uses a specially optimized |
| RootConfig that has a FakeVartree instead of a real vartree. |
| """ |
| pkg = Package( |
| cpv=cpv, |
| built=True, |
| installed=True, |
| metadata=zip(self._db_keys, self._real_vardb.aux_get(cpv, self._db_keys)), |
| root_config=self._pkg_root_config, |
| type_name="installed", |
| ) |
| |
| self._pkg_cache[pkg] = pkg |
| return pkg |
| |
| |
| def grab_global_updates(portdb): |
| retupdates = {} |
| |
| for repo_name in portdb.getRepositories(): |
| repo = portdb.getRepositoryPath(repo_name) |
| updpath = os.path.join(repo, "profiles", "updates") |
| if not os.path.isdir(updpath): |
| continue |
| |
| try: |
| rawupdates = grab_updates(updpath) |
| except portage.exception.DirectoryNotFound: |
| rawupdates = [] |
| upd_commands = [] |
| for mykey, mystat, mycontent in rawupdates: |
| commands, errors = parse_updates(mycontent) |
| upd_commands.extend(commands) |
| retupdates[repo_name] = upd_commands |
| |
| master_repo = portdb.repositories.mainRepo() |
| if master_repo is not None: |
| master_repo = master_repo.name |
| if master_repo in retupdates: |
| retupdates["DEFAULT"] = retupdates[master_repo] |
| |
| return retupdates |
| |
| |
| def perform_global_updates(mycpv, aux_dict, mydb, myupdates): |
| try: |
| pkg = _pkg_str(mycpv, metadata=aux_dict, settings=mydb.settings) |
| except InvalidData: |
| return |
| aux_dict = dict((k, aux_dict[k]) for k in Package._dep_keys) |
| try: |
| mycommands = myupdates[pkg.repo] |
| except KeyError: |
| try: |
| mycommands = myupdates["DEFAULT"] |
| except KeyError: |
| return |
| |
| if not mycommands: |
| return |
| |
| updates = update_dbentries(mycommands, aux_dict, parent=pkg) |
| if updates: |
| mydb.aux_update(mycpv, updates) |