diff options
38 files changed, 1283 insertions, 214 deletions
diff --git a/bin/ebuild.sh b/bin/ebuild.sh index 9c599c01..2affc923 100755 --- a/bin/ebuild.sh +++ b/bin/ebuild.sh @@ -479,6 +479,11 @@ econf() { : ${ECONF_SOURCE:=.} if [ -x "${ECONF_SOURCE}/configure" ]; then + if [[ -n $CONFIG_SHELL && \ + "$(head -n1 "$ECONF_SOURCE/configure")" =~ ^'#!'[[:space:]]*/bin/sh([[:space:]]|$) ]] ; then + sed -e "1s:^#![[:space:]]*/bin/sh:#!$CONFIG_SHELL:" -i "$ECONF_SOURCE/configure" || \ + die "Substition of shebang in '$ECONF_SOURCE/configure' failed" + fi if [ -e /usr/share/gnuconfig/ ]; then find "${WORKDIR}" -type f '(' \ -name config.guess -o -name config.sub ')' -print0 | \ diff --git a/bin/misc-functions.sh b/bin/misc-functions.sh index 7726b9f2..10d19312 100755 --- a/bin/misc-functions.sh +++ b/bin/misc-functions.sh @@ -41,6 +41,8 @@ install_symlink_html_docs() { } install_qa_check() { + local f + cd "${D}" || die "cd failed" export STRIP_MASK @@ -49,6 +51,7 @@ install_qa_check() { ecompress --dequeue # Now we look for all world writable files. + local i for i in $(find "${D}/" -type f -perm -2); do vecho -ne '\a' vecho "QA Security Notice:" @@ -60,7 +63,7 @@ install_qa_check() { if type -P scanelf > /dev/null && ! hasq binchecks ${RESTRICT}; then local qa_var insecure_rpath=0 tmp_quiet=${PORTAGE_QUIET} - local f x + local x # display warnings when using stricter because we die afterwards if has stricter ${FEATURES} ; then @@ -331,6 +334,7 @@ install_qa_check() { fi # Sanity check syntax errors in init.d scripts + local d for d in /etc/conf.d /etc/init.d ; do [[ -d ${D}/${d} ]] || continue for i in "${D}"/${d}/* ; do @@ -343,6 +347,7 @@ install_qa_check() { # this should help to ensure that all (most?) shared libraries are executable # and that all libtool scripts / static libraries are not executable + local j for i in "${D}"opt/*/lib{,32,64} \ "${D}"lib{,32,64} \ "${D}"usr/lib{,32,64} \ @@ -384,6 +389,7 @@ install_qa_check() { # the static library, or gcc will utilize the static lib when linking :(. # http://bugs.gentoo.org/4411 abort="no" + local a s for a in "${D}"usr/lib*/*.a ; do s=${a%.a}.so if [[ ! -e ${s} ]] ; then @@ -532,13 +538,12 @@ install_qa_check() { fi fi - # Compiled python objects do not belong in /usr/share (FHS violation) - # and can be a pain when upgrading python - f=$([ -d "${D}"/usr/share ] && \ - find "${D}"usr/share -name '*.py[co]' | sed "s:${D}:/:") + f=$(find "${D}" -name '*.py[co]' | sed "s:${D}:/:") if [[ -n ${f} ]] ; then vecho -ne '\a\n' - eqawarn "QA Notice: Precompiled python object files do not belong in /usr/share" + eqawarn "QA Notice: Byte-compiled Python modules have been found. python_mod_optimize()" + eqawarn " and python_mod_cleanup() functions python.eclass should be used to" + eqawarn " handle byte-compiled Python modules." eqawarn "${f}" vecho -ne '\a\n' fi @@ -550,7 +555,7 @@ install_qa_check() { [[ -x /usr/bin/file && -x /usr/bin/find ]] && \ [[ -n ${MULTILIB_STRICT_DIRS} && -n ${MULTILIB_STRICT_DENY} ]] then - local abort=no firstrun=yes + local abort=no dir file firstrun=yes MULTILIB_STRICT_EXEMPT=$(echo ${MULTILIB_STRICT_EXEMPT} | sed -e 's:\([(|)]\):\\\1:g') for dir in ${MULTILIB_STRICT_DIRS} ; do [[ -d ${D}/${dir} ]] || continue @@ -578,6 +583,7 @@ install_mask() { # we don't want globbing for initial expansion, but afterwards, we do local shopts=$- set -o noglob + local no_inst for no_inst in ${install_mask}; do set +o noglob quiet_mode || einfo "Removing ${no_inst}" @@ -620,6 +626,7 @@ preinst_mask() { cd "${T}" # remove man pages, info pages, docs if requested + local f for f in man info doc; do if hasq no${f} $FEATURES; then INSTALL_MASK="${INSTALL_MASK} /usr/share/${f}" @@ -676,7 +683,7 @@ preinst_suid_scan() { fi # total suid control. if hasq suidctl $FEATURES; then - local sfconf + local i sfconf x sfconf=${PORTAGE_CONFIGROOT}etc/portage/suidctl.conf # sandbox prevents us from writing directly # to files outside of the sandbox, but this @@ -845,6 +852,7 @@ if [ -n "${MISC_FUNCTIONS_ARGS}" ]; then for x in ${MISC_FUNCTIONS_ARGS}; do ${x} done + unset x fi [ -n "${EBUILD_EXIT_STATUS_FILE}" ] && \ diff --git a/bin/repoman b/bin/repoman index 53b9ad0c..0115c288 100755 --- a/bin/repoman +++ b/bin/repoman @@ -21,8 +21,6 @@ import sys import tempfile import time import platform -import xml.etree.ElementTree -from xml.parsers.expat import ExpatError try: from urllib.request import urlopen as urllib_request_urlopen @@ -32,9 +30,6 @@ except ImportError: from itertools import chain from stat import S_ISDIR -if not hasattr(__builtins__, "set"): - from sets import Set as set - try: import portage except ImportError: @@ -42,6 +37,18 @@ except ImportError: sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")) import portage portage._disable_legacy_globals() + +try: + import xml.etree.ElementTree + from xml.parsers.expat import ExpatError +except ImportError: + msg = ["Please enable python's \"xml\" USE flag in order to use repoman."] + from portage.output import EOutput + out = EOutput() + for line in msg: + out.eerror(line) + sys.exit(1) + from portage import os from portage import subprocess_getstatusoutput from portage import _encodings @@ -328,7 +335,7 @@ qahelp={ "IUSE.undefined":"This ebuild does not define IUSE (style guideline says to define IUSE even when empty)", "LICENSE.invalid":"This ebuild is listing a license that doesnt exist in portages license/ dir.", "KEYWORDS.invalid":"This ebuild contains KEYWORDS that are not listed in profiles/arch.list or for which no valid profile was found", - "RDEPEND.implicit":"RDEPEND is unset in the ebuild which triggers implicit RDEPEND=$DEPEND assignment", + "RDEPEND.implicit":"RDEPEND is unset in the ebuild which triggers implicit RDEPEND=$DEPEND assignment (prior to EAPI 4)", "RDEPEND.suspect":"RDEPEND contains a package that usually only belongs in DEPEND.", "RESTRICT.invalid":"This ebuild contains invalid RESTRICT values.", "digest.assumed":"Existing digest must be assumed correct (Package level only)", @@ -339,6 +346,7 @@ qahelp={ "ebuild.majorsyn":"This ebuild has a major syntax error that may cause the ebuild to fail partially or fully", "ebuild.minorsyn":"This ebuild has a minor syntax error that contravenes gentoo coding style", "ebuild.badheader":"This ebuild has a malformed header", + "eprefixify.defined":"The ebuild uses eprefixify, but does not inherit the prefix eclass", "manifest.bad":"Manifest has missing or incorrect digests", "metadata.missing":"Missing metadata.xml files", "metadata.bad":"Bad metadata.xml files", diff --git a/man/ebuild.5 b/man/ebuild.5 index 5c71cbea..94f49d70 100644 --- a/man/ebuild.5 +++ b/man/ebuild.5 @@ -286,9 +286,15 @@ and explicitly disallow them from being temporarily installed simultaneously during a series of upgrades. This syntax is supported beginning with \fBEAPI 2\fR. .br -\fI*\fR means match any version of the package so long as the specified base -is matched. So with a version of '2*', we can match '2.1', '2.2', '2.2.1', +\fI*\fR means match any version of the package so long +as the specified string prefix is matched. So with a +version of '2*', we can match '2.1', '2.2', '2.2.1', etc... and not match version '1.0', '3.0', '4.1', etc... +Beware that, due to the string matching nature, '20' +will also be matched by '2*'. The version part +that comes before the '*' must be a valid version in the absence of the '*'. +For example, '2' is a valid version and '2.' is not. Therefore, '2*' is +allowed and '2.*' is not. .br \fI~\fR means match any revision of the base version specified. So in the above example, we would match versions '1.0.2a', '1.0.2a\-r1', '1.0.2a\-r2', diff --git a/man/emerge.1 b/man/emerge.1 index 572002d3..73c90d3f 100644 --- a/man/emerge.1 +++ b/man/emerge.1 @@ -284,6 +284,15 @@ acceptance of the first choice. This option is intended to be set in the \fBmake.conf\fR(5) \fBEMERGE_DEFAULT_OPTS\fR variable. .TP +.BR "\-\-autounmask[=n]" +Automatically unmask packages. If any configuration +changes are required, then they will be displayed +after the merge list and emerge will immediately +abort. If the displayed configuration changes are +satisfactory, you should copy and paste them into +the specified configuration file(s). Currently, +this only works for unstable KEYWORDS masks. +.TP .BR \-\-backtrack=COUNT Specifies an integer number of times to backtrack if dependency calculation fails due to a conflict or an diff --git a/man/repoman.1 b/man/repoman.1 index 58165bb7..ad4c74cb 100644 --- a/man/repoman.1 +++ b/man/repoman.1 @@ -210,6 +210,10 @@ Masked ebuilds with RDEPEND settings (matched against *all* ebuilds) in developi .B RDEPEND.badtilde RDEPEND uses the ~ dep operator with a non-zero revision part, which is useless (the revision is ignored) .TP +.B RDEPEND.implicit +RDEPEND is unset in the ebuild which triggers implicit RDEPEND=$DEPEND +assignment (prior to EAPI 4) +.TP .B RDEPEND.suspect RDEPEND contains a package that usually only belongs in DEPEND .TP @@ -282,6 +286,9 @@ PATCHES variable should be a bash array to ensure white space safety Error generating cache entry for ebuild; typically caused by ebuild syntax error or digest verification failure. .TP +.B eprefixify.defined +The ebuild uses eprefixify, but does not inherit the prefix eclass +.TP .B file.UTF8 File is not UTF8 compliant .TP diff --git a/pym/_emerge/PackageUninstall.py b/pym/_emerge/PackageUninstall.py index 58a1717a..6f528663 100644 --- a/pym/_emerge/PackageUninstall.py +++ b/pym/_emerge/PackageUninstall.py @@ -27,8 +27,8 @@ class PackageUninstall(AsynchronousTask): else: self.returncode = os.EX_OK - if retval == 1: - self.world_atom(self.pkg) + if retval == 1: + self.world_atom(self.pkg) self.wait() diff --git a/pym/_emerge/PipeReader.py b/pym/_emerge/PipeReader.py index be958bcb..604fbf29 100644 --- a/pym/_emerge/PipeReader.py +++ b/pym/_emerge/PipeReader.py @@ -71,7 +71,7 @@ class PipeReader(AbstractPollTask): buf = array.array('B') try: buf.fromfile(f, self._bufsize) - except EOFError: + except (EOFError, IOError): pass if buf: diff --git a/pym/_emerge/Scheduler.py b/pym/_emerge/Scheduler.py index 0de656c7..067aa1a0 100644 --- a/pym/_emerge/Scheduler.py +++ b/pym/_emerge/Scheduler.py @@ -345,6 +345,7 @@ class Scheduler(PollScheduler): def _set_digraph(self, digraph): if "--nodeps" in self.myopts or \ + digraph is None or \ (self._max_jobs is not True and self._max_jobs < 2): # save some memory self._digraph = None diff --git a/pym/_emerge/SpawnProcess.py b/pym/_emerge/SpawnProcess.py index bacbc2f3..51cd04fc 100644 --- a/pym/_emerge/SpawnProcess.py +++ b/pym/_emerge/SpawnProcess.py @@ -148,7 +148,6 @@ class SpawnProcess(SubProcess): buf = array.array('B') try: buf.fromfile(files.process, self._bufsize) - # EOFError was raised in Python <2.6.6 and <2.7.1. except (EOFError, IOError): pass @@ -210,7 +209,7 @@ class SpawnProcess(SubProcess): buf = array.array('B') try: buf.fromfile(self._files.process, self._bufsize) - except EOFError: + except (EOFError, IOError): pass if buf: diff --git a/pym/_emerge/actions.py b/pym/_emerge/actions.py index 70b091e5..bd167a0c 100644 --- a/pym/_emerge/actions.py +++ b/pym/_emerge/actions.py @@ -515,7 +515,7 @@ def action_config(settings, trees, myopts, myfiles): print() def action_depclean(settings, trees, ldpath_mtimes, - myopts, action, myfiles, spinner): + myopts, action, myfiles, spinner, scheduler=None): # Kill packages that aren't explicitly merged or are required as a # dependency of another package. World file is explicit. @@ -576,7 +576,8 @@ def action_depclean(settings, trees, ldpath_mtimes, if cleanlist: unmerge(root_config, myopts, "unmerge", - cleanlist, ldpath_mtimes, ordered=ordered) + cleanlist, ldpath_mtimes, ordered=ordered, + scheduler=scheduler) if action == "prune": return @@ -2650,7 +2651,7 @@ def action_uninstall(settings, trees, ldpath_mtimes, for line in textwrap.wrap(msg, 70): writemsg_level("!!! %s\n" % (line,), level=logging.ERROR, noiselevel=-1) - for i in e[0]: + for i in e.args[0]: writemsg_level(" %s\n" % colorize("INFORM", i), level=logging.ERROR, noiselevel=-1) writemsg_level("\n", level=logging.ERROR, noiselevel=-1) @@ -2723,18 +2724,28 @@ def action_uninstall(settings, trees, ldpath_mtimes, for line in textwrap.wrap(msg, 72): out.ewarn(line) + if action == 'deselect': + return action_deselect(settings, trees, opts, valid_atoms) + + # Create a Scheduler for calls to unmerge(), in order to cause + # redirection of ebuild phase output to logs as required for + # options such as --quiet. + sched = Scheduler(settings, trees, None, opts, + spinner, [], [], None) + sched._background = sched._background_mode() + sched._status_display.quiet = True + if action in ('clean', 'unmerge') or \ (action == 'prune' and "--nodeps" in opts): # When given a list of atoms, unmerge them in the order given. ordered = action == 'unmerge' unmerge(trees[settings["ROOT"]]['root_config'], opts, action, - valid_atoms, ldpath_mtimes, ordered=ordered) + valid_atoms, ldpath_mtimes, ordered=ordered, + scheduler=sched._sched_iface) rval = os.EX_OK - elif action == 'deselect': - rval = action_deselect(settings, trees, opts, valid_atoms) else: rval = action_depclean(settings, trees, ldpath_mtimes, - opts, action, valid_atoms, spinner) + opts, action, valid_atoms, spinner, scheduler=sched._sched_iface) return rval diff --git a/pym/_emerge/depgraph.py b/pym/_emerge/depgraph.py index 684b5b64..d31fcb8f 100644 --- a/pym/_emerge/depgraph.py +++ b/pym/_emerge/depgraph.py @@ -1,4 +1,4 @@ -# Copyright 1999-2009 Gentoo Foundation +# Copyright 1999-2010 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 from __future__ import print_function @@ -20,6 +20,8 @@ from portage.dep import Atom from portage.output import bold, blue, colorize, create_color_func, darkblue, \ darkgreen, green, nc_len, red, teal, turquoise, yellow bad = create_color_func("BAD") +from portage.package.ebuild.getmaskingstatus import \ + _getmaskingstatus, _MaskReason from portage.sets import SETPREFIX from portage.sets.base import InternalPackageSet from portage.util import cmp_sort_key, writemsg, writemsg_stdout @@ -66,7 +68,10 @@ class _frozen_depgraph_config(object): if settings.get("PORTAGE_DEBUG", "") == "1": self.edebug = 1 self.spinner = spinner - self._running_root = trees["/"]["root_config"] + if "_test_" in myopts and "/" not in trees: + self._running_root = trees[self.target_root]["root_config"] + else: + self._running_root = trees["/"]["root_config"] self._opts_no_restart = frozenset(["--buildpkgonly", "--fetchonly", "--fetch-all-uri", "--pretend"]) self.pkgsettings = {} @@ -106,7 +111,7 @@ class _frozen_depgraph_config(object): class _dynamic_depgraph_config(object): def __init__(self, depgraph, myparams, allow_backtracking, - runtime_pkg_mask): + runtime_pkg_mask, needed_unstable_keywords, needed_use_config_changes): self.myparams = myparams.copy() self._vdb_loaded = False self._allow_backtracking = allow_backtracking @@ -182,6 +187,21 @@ class _dynamic_depgraph_config(object): else: runtime_pkg_mask = dict((k, v.copy()) for (k, v) in \ runtime_pkg_mask.items()) + + if needed_unstable_keywords is None: + self._needed_unstable_keywords = set() + else: + self._needed_unstable_keywords = needed_unstable_keywords.copy() + + if needed_use_config_changes is None: + self._needed_use_config_changes = {} + else: + self._needed_use_config_changes = \ + dict((k.copy(), (v[0].copy(), v[1].copy())) for (k, v) in \ + needed_use_config_changes.items()) + + self._autounmask = depgraph._frozen_config.myopts.get('--autounmask', 'n') == True + self._runtime_pkg_mask = runtime_pkg_mask self._need_restart = False @@ -252,13 +272,14 @@ class depgraph(object): _dep_keys = ["DEPEND", "RDEPEND", "PDEPEND"] def __init__(self, settings, trees, myopts, myparams, spinner, - frozen_config=None, runtime_pkg_mask=None, allow_backtracking=False): + frozen_config=None, runtime_pkg_mask=None, needed_unstable_keywords=None, \ + needed_use_config_changes=None, allow_backtracking=False): if frozen_config is None: frozen_config = _frozen_depgraph_config(settings, trees, myopts, spinner) self._frozen_config = frozen_config self._dynamic_config = _dynamic_depgraph_config(self, myparams, - allow_backtracking, runtime_pkg_mask) + allow_backtracking, runtime_pkg_mask, needed_unstable_keywords, needed_use_config_changes) self._select_atoms = self._select_atoms_highest_available self._select_package = self._select_pkg_highest_available @@ -1109,7 +1130,7 @@ class depgraph(object): myroot = pkg.root mykey = pkg.cpv metadata = pkg.metadata - myuse = pkg.use.enabled + myuse = self._pkg_use_enabled(pkg) jbigkey = pkg depth = pkg.depth + 1 removal_action = "remove" in self._dynamic_config.myparams @@ -1190,7 +1211,7 @@ class depgraph(object): dep_string = portage.dep.paren_normalize( portage.dep.use_reduce( portage.dep.paren_reduce(dep_string), - uselist=pkg.use.enabled)) + uselist=self._pkg_use_enabled(pkg))) dep_string = list(self._queue_disjunctive_deps( pkg, dep_root, dep_priority, dep_string)) @@ -1252,7 +1273,7 @@ class depgraph(object): try: selected_atoms = self._select_atoms(dep_root, - dep_string, myuse=pkg.use.enabled, parent=pkg, + dep_string, myuse=self._pkg_use_enabled(pkg), parent=pkg, strict=strict, priority=dep_priority) except portage.exception.InvalidDependString as e: show_invalid_depstring_notice(pkg, dep_string, str(e)) @@ -1282,7 +1303,7 @@ class depgraph(object): inst_pkgs = vardb.match_pkgs(atom) if inst_pkgs: for inst_pkg in inst_pkgs: - if inst_pkg.visible: + if self._pkg_visibility_check(inst_pkg): # highest visible mypriority.satisfied = inst_pkg break @@ -1328,7 +1349,7 @@ class depgraph(object): inst_pkgs = vardb.match_pkgs(atom) if inst_pkgs: for inst_pkg in inst_pkgs: - if inst_pkg.visible: + if self._pkg_visibility_check(inst_pkg): # highest visible mypriority.satisfied = inst_pkg break @@ -1971,6 +1992,13 @@ class depgraph(object): except self._unknown_internal_error: return False, myfavorites + if set(self._dynamic_config.digraph.nodes.keys()).intersection( \ + set(self._dynamic_config._needed_unstable_keywords)) or \ + set(self._dynamic_config.digraph.nodes.keys()).intersection( \ + set(self._dynamic_config._needed_use_config_changes.keys())) : + #We failed if the user needs to change the configuration + return False, myfavorites + # We're true here unless we are missing binaries. return (not missing,myfavorites) @@ -2052,7 +2080,7 @@ class depgraph(object): dep_str = " ".join(pkg.metadata[k] for k in blocker_dep_keys) try: selected_atoms = self._select_atoms( - pkg.root, dep_str, pkg.use.enabled, + pkg.root, dep_str, self._pkg_use_enabled(pkg), parent=pkg, strict=True) except portage.exception.InvalidDependString: continue @@ -2107,11 +2135,18 @@ class depgraph(object): myuse=None, parent=None, strict=True, trees=None, priority=None): """This will raise InvalidDependString if necessary. If trees is None then self._dynamic_config._filtered_trees is used.""" + pkgsettings = self._frozen_config.pkgsettings[root] if trees is None: trees = self._dynamic_config._filtered_trees + mytrees = trees[root] atom_graph = digraph() if True: + # Temporarily disable autounmask so that || preferences + # account for masking and USE settings. + _autounmask_backup = self._dynamic_config._autounmask + self._dynamic_config._autounmask = False + mytrees["pkg_use_enabled"] = self._pkg_use_enabled try: if parent is not None: trees[root]["parent"] = parent @@ -2124,6 +2159,8 @@ class depgraph(object): pkgsettings, myuse=myuse, myroot=root, trees=trees) finally: + self._dynamic_config._autounmask = _autounmask_backup + del mytrees["pkg_use_enabled"] if parent is not None: trees[root].pop("parent") trees[root].pop("atom_graph") @@ -2221,7 +2258,7 @@ class depgraph(object): masked_pkg_instances.add(pkg) if atom.unevaluated_atom.use: if not pkg.iuse.is_valid_flag(atom.unevaluated_atom.use.required) \ - or atom.violated_conditionals(pkg.use.enabled).use: + or atom.violated_conditionals(self._pkg_use_enabled(pkg)).use: missing_use.append(pkg) if not mreasons: continue @@ -2239,7 +2276,7 @@ class depgraph(object): missing_use_reasons = [] missing_iuse_reasons = [] for pkg in missing_use: - use = pkg.use.enabled + use = self._pkg_use_enabled(pkg) missing_iuse = pkg.iuse.get_missing_iuse(atom.use.required) mreasons = [] if missing_iuse: @@ -2261,7 +2298,7 @@ class depgraph(object): # Lets see if the violated use deps are conditional. # If so, suggest to change them on the parent. mreasons = [] - violated_atom = atom.unevaluated_atom.violated_conditionals(pkg.use.enabled, myparent.use.enabled) + violated_atom = atom.unevaluated_atom.violated_conditionals(self._pkg_use_enabled(pkg), myparent.use.enabled) if not (violated_atom.use.enabled or violated_atom.use.disabled): #all violated use deps are conditional changes = [] @@ -2296,11 +2333,14 @@ class depgraph(object): break elif unmasked_iuse_reasons: - if missing_use_reasons: - # All packages with required IUSE are masked, - # so display a normal masking message. - pass - else: + masked_with_iuse = False + for pkg in masked_pkg_instances: + if not pkg.iuse.get_missing_iuse(atom.use.required): + # Package(s) with required IUSE are masked, + # so display a normal masking message. + masked_with_iuse = True + break + if not masked_with_iuse: show_missing_use = unmasked_iuse_reasons mask_docs = False @@ -2467,12 +2507,162 @@ class depgraph(object): pkg, existing = ret if pkg is not None: settings = pkg.root_config.settings - if pkg.visible and not (pkg.installed and \ + if self._pkg_visibility_check(pkg) and not (pkg.installed and \ settings._getMissingKeywords(pkg.cpv, pkg.metadata)): self._dynamic_config._visible_pkgs[pkg.root].cpv_inject(pkg) return ret + def _want_installed_pkg(self, pkg): + """ + Given an installed package returned from select_pkg, return + True if the user has not explicitly requested for this package + to be replaced (typically via an atom on the command line). + """ + if "selective" not in self._dynamic_config.myparams and \ + pkg.root == self._frozen_config.target_root: + try: + next(self._iter_atoms_for_pkg(pkg)) + except StopIteration: + pass + except portage.exception.InvalidDependString: + pass + else: + return False + return True + def _select_pkg_highest_available_imp(self, root, atom, onlydeps=False): + pkg, existing = self._wrapped_select_pkg_highest_available_imp(root, atom, onlydeps=onlydeps) + + default_selection = (pkg, existing) + + if self._dynamic_config._autounmask is True: + if pkg is not None and \ + pkg.installed and \ + not self._want_installed_pkg(pkg): + pkg = None + + for allow_unstable_keywords in False, True: + if pkg is not None: + break + + pkg, existing = \ + self._wrapped_select_pkg_highest_available_imp( + root, atom, onlydeps=onlydeps, + allow_use_changes=True, allow_unstable_keywords=allow_unstable_keywords) + + if pkg is not None and \ + pkg.installed and \ + not self._want_installed_pkg(pkg): + pkg = None + + if pkg is not None and not pkg.visible: + self._dynamic_config._needed_unstable_keywords.add(pkg) + + if self._dynamic_config._need_restart: + return None, None + + if pkg is None: + # This ensures that we can fall back to an installed package + # that may have been rejected in the autounmask path above. + return default_selection + + return pkg, existing + + def _pkg_visibility_check(self, pkg, allow_unstable_keywords=False): + if pkg.visible: + return True + + if pkg in self._dynamic_config._needed_unstable_keywords: + return True + + if not allow_unstable_keywords: + return False + + pkgsettings = self._frozen_config.pkgsettings[pkg.root] + root_config = self._frozen_config.roots[pkg.root] + mreasons = _get_masking_status(pkg, pkgsettings, root_config) + if len(mreasons) == 1 and \ + mreasons[0].hint == 'unstable keyword': + return True + else: + return False + + def _pkg_use_enabled(self, pkg, target_use=None): + """ + If target_use is None, returns pkg.use.enabled + changes in _needed_use_config_changes. + If target_use is given, the need changes are computed to make the package useable. + Example: target_use = { "foo": True, "bar": False } + The flags target_use must be in the pkg's IUSE. + """ + needed_use_config_change = self._dynamic_config._needed_use_config_changes.get(pkg) + + if target_use is None: + if needed_use_config_change is None: + return pkg.use.enabled + else: + return needed_use_config_change[0] + + if needed_use_config_change is not None: + old_use = needed_use_config_change[0] + new_use = set() + old_changes = needed_use_config_change[1] + new_changes = old_changes.copy() + else: + old_use = pkg.use.enabled + new_use = set() + old_changes = {} + new_changes = {} + + for flag, state in target_use.items(): + if state: + if flag not in old_use: + if new_changes.get(flag) == False: + return old_use + new_changes[flag] = True + new_use.add(flag) + else: + if flag in old_use: + if new_changes.get(flag) == True: + return old_use + new_changes[flag] = False + new_use.update(old_use.difference(target_use.keys())) + + def want_restart_for_use_change(pkg, new_use): + if pkg not in self._dynamic_config.digraph.nodes: + return False + + for key in "DEPEND", "RDEPEND", "PDEPEND", "LICENSE": + dep = pkg.metadata[key] + old_val = set(portage.dep.paren_normalize( \ + portage.dep.use_reduce(portage.dep.paren_reduce(dep), pkg.use.enabled))) + new_val = set(portage.dep.paren_normalize( \ + portage.dep.use_reduce(portage.dep.paren_reduce(dep), new_use))) + + if old_val != new_val: + return True + + parent_atoms = self._dynamic_config._parent_atoms.get(pkg) + if not parent_atoms: + return False + + new_use, changes = self._dynamic_config._needed_use_config_changes.get(pkg) + for ppkg, atom in parent_atoms: + if not atom.use or \ + not atom.use.required.intersection(changes.keys()): + continue + else: + return True + + return False + + if new_changes != old_changes: + self._dynamic_config._needed_use_config_changes[pkg] = (new_use, new_changes) + if want_restart_for_use_change(pkg, new_use): + self._dynamic_config._need_restart = True + return new_use + + def _wrapped_select_pkg_highest_available_imp(self, root, atom, onlydeps=False, \ + allow_use_changes=False, allow_unstable_keywords=False): root_config = self._frozen_config.roots[root] pkgsettings = self._frozen_config.pkgsettings[root] dbs = self._dynamic_config._filtered_trees[root]["dbs"] @@ -2569,7 +2759,7 @@ class depgraph(object): # were installed can be automatically downgraded # to an unmasked version. - if not pkg.visible: + if not self._pkg_visibility_check(pkg, allow_unstable_keywords=allow_unstable_keywords): continue # Enable upgrade or downgrade to a version @@ -2603,7 +2793,8 @@ class depgraph(object): except portage.exception.PackageNotFound: continue else: - if not pkg_eb.visible: + if not self._pkg_visibility_check(pkg_eb, \ + allow_unstable_keywords=allow_unstable_keywords): continue # Calculation of USE for unbuilt ebuilds is relatively @@ -2630,11 +2821,21 @@ class depgraph(object): # since IUSE cannot be adjusted by the user. continue - if atom.use.enabled.difference(pkg.use.enabled): + if allow_use_changes: + target_use = {} + for flag in atom.use.enabled: + target_use[flag] = True + for flag in atom.use.disabled: + target_use[flag] = False + use = self._pkg_use_enabled(pkg, target_use) + else: + use = self._pkg_use_enabled(pkg) + + if atom.use.enabled.difference(use): if not pkg.built: packages_with_invalid_use_config.append(pkg) continue - if atom.use.disabled.intersection(pkg.use.enabled): + if atom.use.disabled.intersection(use): if not pkg.built: packages_with_invalid_use_config.append(pkg) continue @@ -2675,7 +2876,7 @@ class depgraph(object): "--reinstall" in self._frozen_config.myopts or \ "--binpkg-respect-use" in self._frozen_config.myopts): iuses = pkg.iuse.all - old_use = pkg.use.enabled + old_use = self._pkg_use_enabled(pkg) if myeb: pkgsettings.setcpv(myeb) else: @@ -2704,7 +2905,7 @@ class depgraph(object): old_use = vardb.aux_get(cpv, ["USE"])[0].split() old_iuse = set(filter_iuse_defaults( vardb.aux_get(cpv, ["IUSE"])[0].split())) - cur_use = pkg.use.enabled + cur_use = self._pkg_use_enabled(pkg) cur_iuse = pkg.iuse.all reinstall_for_flags = \ self._reinstall_for_flags( @@ -2787,11 +2988,13 @@ class depgraph(object): if avoid_update: for pkg in matched_packages: - if pkg.installed and pkg.visible: + if pkg.installed and self._pkg_visibility_check(pkg, \ + allow_unstable_keywords=allow_unstable_keywords): return pkg, existing_node bestmatch = portage.best( - [pkg.cpv for pkg in matched_packages if pkg.visible]) + [pkg.cpv for pkg in matched_packages \ + if self._pkg_visibility_check(pkg, allow_unstable_keywords=allow_unstable_keywords)]) if not bestmatch: # all are masked, so ignore visibility bestmatch = portage.best( @@ -2958,7 +3161,7 @@ class depgraph(object): self._frozen_config._pkg_cache[pkg] = pkg - if not pkg.visible and \ + if not self._pkg_visibility_check(pkg) and \ 'LICENSE' in pkg.masks and len(pkg.masks) == 1: slot_key = (pkg.root, pkg.slot_atom) other_pkg = self._frozen_config._highest_license_masked.get(slot_key) @@ -3014,7 +3217,7 @@ class depgraph(object): # packages masked by license, since the user likely wants # to adjust ACCEPT_LICENSE. if pkg in final_db: - if not pkg.visible and \ + if not self._pkg_visibility_check(pkg) and \ (pkg_in_graph or 'LICENSE' in pkg.masks): self._dynamic_config._masked_installed.add(pkg) else: @@ -3083,7 +3286,7 @@ class depgraph(object): portage.dep._dep_check_strict = False try: success, atoms = portage.dep_check(depstr, - final_db, pkgsettings, myuse=pkg.use.enabled, + final_db, pkgsettings, myuse=self._pkg_use_enabled(pkg), trees=self._dynamic_config._graph_trees, myroot=myroot) except Exception as e: if isinstance(e, SystemExit): @@ -4394,7 +4597,7 @@ class depgraph(object): os.path.dirname(ebuild_path))) else: repo_path_real = portdb.getRepositoryPath(repo_name) - pkg_use = list(pkg.use.enabled) + pkg_use = list(self._pkg_use_enabled(pkg)) if not pkg.built and pkg.operation == 'merge' and \ 'fetch' in pkg.metadata.restrict: fetch = red("F") @@ -4485,7 +4688,7 @@ class depgraph(object): forced_flags.update(pkgsettings.useforce) forced_flags.update(pkgsettings.usemask) - cur_use = [flag for flag in pkg.use.enabled \ + cur_use = [flag for flag in self._pkg_use_enabled(pkg) \ if flag in pkg.iuse.all] cur_iuse = sorted(pkg.iuse.all) @@ -5049,6 +5252,74 @@ class depgraph(object): else: self._show_missed_update() + def get_dep_chain(pkg): + traversed_nodes = set() + msg = "#" + node = pkg + first = True + while node is not None: + traversed_nodes.add(node) + if node is not pkg: + if first: + first = False + else: + msg += ", " + msg += 'required by =%s' % node.cpv + + if node not in self._dynamic_config.digraph: + # The parent is not in the graph due to backtracking. + break + + # When traversing to parents, prefer arguments over packages + # since arguments are root nodes. Never traverse the same + # package twice, in order to prevent an infinite loop. + selected_parent = None + for parent in self._dynamic_config.digraph.parent_nodes(node): + if isinstance(parent, DependencyArg): + if first: + first = False + else: + msg += ", " + msg += 'required by %s (argument)' % str(parent) + selected_parent = None + break + if parent not in traversed_nodes: + selected_parent = parent + node = selected_parent + msg += "\n" + return msg + + unstable_keyword_msg = [] + for pkg in self._dynamic_config._needed_unstable_keywords: + self._show_merge_list() + if pkg in self._dynamic_config.digraph.nodes.keys(): + pkgsettings = self._frozen_config.pkgsettings[pkg.root] + unstable_keyword_msg.append(get_dep_chain(pkg)) + unstable_keyword_msg.append("=%s ~%s\n" % (pkg.cpv, pkgsettings["ACCEPT_KEYWORDS"])) + + use_changes_msg = [] + for pkg, needed_use_config_change in self._dynamic_config._needed_use_config_changes.items(): + self._show_merge_list() + if pkg in self._dynamic_config.digraph.nodes.keys(): + changes = needed_use_config_change[1] + adjustments = [] + for flag, state in changes.items(): + if state: + adjustments.append(flag) + else: + adjustments.append("-" + flag) + use_changes_msg.append("=%s %s\n" % (pkg.cpv, " ".join(adjustments))) + + if unstable_keyword_msg: + writemsg_stdout("\nThe following " + colorize("BAD", "keyword changes") + \ + " are necessary to proceed:\n", noiselevel=-1) + writemsg_stdout("".join(unstable_keyword_msg), noiselevel=-1) + + if use_changes_msg: + writemsg_stdout("\nThe following " + colorize("BAD", "USE changes") + \ + " are necessary to proceed:\n", noiselevel=-1) + writemsg_stdout("".join(use_changes_msg), noiselevel=-1) + # TODO: Add generic support for "set problem" handlers so that # the below warnings aren't special cases for world only. @@ -5247,7 +5518,7 @@ class depgraph(object): self._frozen_config.excluded_pkgs.findAtomForPackage(pkg): continue - if "merge" == pkg.operation and not pkg.visible: + if "merge" == pkg.operation and not self._pkg_visibility_check(pkg): if skip_masked: masked_tasks.append(Dependency(root=pkg.root, parent=pkg)) else: @@ -5440,8 +5711,16 @@ class depgraph(object): def need_restart(self): return self._dynamic_config._need_restart - def get_runtime_pkg_mask(self): - return self._dynamic_config._runtime_pkg_mask.copy() + def get_backtrack_parameters(self): + return { + "needed_unstable_keywords": + self._dynamic_config._needed_unstable_keywords.copy(), \ + "runtime_pkg_mask": + self._dynamic_config._runtime_pkg_mask.copy(), + "needed_use_config_changes": + self._dynamic_config._needed_use_config_changes.copy() + } + class _dep_check_composite_db(dbapi): """ @@ -5672,7 +5951,8 @@ def _backtrack_depgraph(settings, trees, myopts, myparams, myaction, myfiles, spinner): backtrack_max = myopts.get('--backtrack', 5) - runtime_pkg_mask = None + backtrack_parameters = {} + needed_unstable_keywords = None allow_backtracking = backtrack_max > 0 backtracked = 0 frozen_config = _frozen_depgraph_config(settings, trees, @@ -5681,11 +5961,11 @@ def _backtrack_depgraph(settings, trees, myopts, myparams, mydepgraph = depgraph(settings, trees, myopts, myparams, spinner, frozen_config=frozen_config, allow_backtracking=allow_backtracking, - runtime_pkg_mask=runtime_pkg_mask) + **backtrack_parameters) success, favorites = mydepgraph.select_files(myfiles) if not success: if mydepgraph.need_restart() and backtracked < backtrack_max: - runtime_pkg_mask = mydepgraph.get_runtime_pkg_mask() + backtrack_parameters = mydepgraph.get_backtrack_parameters() backtracked += 1 elif backtracked and allow_backtracking: if "--debug" in myopts: @@ -5695,7 +5975,10 @@ def _backtrack_depgraph(settings, trees, myopts, myparams, # Backtracking failed, so disable it and do # a plain dep calculation + error message. allow_backtracking = False - runtime_pkg_mask = None + #Don't reset needed_unstable_keywords here, since we don't want to + #send the user through a "one step at a time" unmasking session for + #no good reason. + backtrack_parameters.pop('runtime_pkg_mask', None) else: break else: @@ -5884,21 +6167,26 @@ def show_blocker_docs_link(): print() def get_masking_status(pkg, pkgsettings, root_config, myrepo=None): + return [mreason.message for \ + mreason in _get_masking_status(pkg, pkgsettings, root_config, myrepo)] - mreasons = portage.getmaskingstatus( +def _get_masking_status(pkg, pkgsettings, root_config, myrepo=None): + mreasons = _getmaskingstatus( pkg, settings=pkgsettings, portdb=root_config.trees["porttree"].dbapi, myrepo=myrepo) if not pkg.installed: if not pkgsettings._accept_chost(pkg.cpv, pkg.metadata): - mreasons.append("CHOST: %s" % \ - pkg.metadata["CHOST"]) + mreasons.append(_MaskReason("CHOST", "CHOST: %s" % \ + pkg.metadata["CHOST"])) if pkg.invalid: for msg_type, msgs in pkg.invalid.items(): for msg in msgs: - mreasons.append("invalid: %s" % (msg,)) + mreasons.append( + _MaskReason("invalid", "invalid: %s" % (msg,))) if not pkg.metadata["SLOT"]: - mreasons.append("invalid: SLOT is undefined") + mreasons.append( + _MaskReason("invalid", "SLOT: undefined")) return mreasons diff --git a/pym/_emerge/help.py b/pym/_emerge/help.py index 43e9794e..72bb56d4 100644 --- a/pym/_emerge/help.py +++ b/pym/_emerge/help.py @@ -291,6 +291,17 @@ def help(myopts, havecolor=1): "EMERGE_DEFAULT_OPTS variable." for line in wrap(desc, desc_width): print(desc_indent + line) + print() + print(" " + green("--autounmask") + "[=%s]" % turquoise("n")) + desc = "Automatically unmask packages. If any configuration " + \ + "changes are required, then they will be displayed " + \ + "after the merge list and emerge will immediately " + \ + "abort. If the displayed configuration changes are " + \ + "satisfactory, you should copy and paste them into " + \ + "the specified configuration file(s). Currently, " + \ + "this only works for unstable KEYWORDS masks." + for line in wrap(desc, desc_width): + print(desc_indent + line) print() print(" " + green("--backtrack") + " " + turquoise("COUNT")) desc = "Specifies an integer number of times to backtrack if " + \ diff --git a/pym/_emerge/main.py b/pym/_emerge/main.py index b7008658..974b5cf3 100644 --- a/pym/_emerge/main.py +++ b/pym/_emerge/main.py @@ -388,6 +388,7 @@ def insert_optional_args(args): new_args = [] default_arg_opts = { + '--autounmask' : ('n',), '--complete-graph' : ('n',), '--deep' : valid_integers, '--depclean-lib-check' : ('n',), @@ -515,6 +516,13 @@ def parse_opts(tmpcmdline, silent=False): longopt_aliases = {"--cols":"--columns", "--skip-first":"--skipfirst"} argument_options = { + + "--autounmask": { + "help" : "automatically unmask packages", + "type" : "choice", + "choices" : ("True", "n") + }, + "--accept-properties": { "help":"temporarily override ACCEPT_PROPERTIES", "action":"store" @@ -735,6 +743,9 @@ def parse_opts(tmpcmdline, silent=False): myoptions, myargs = parser.parse_args(args=tmpcmdline) + if myoptions.autounmask in ("True",): + myoptions.autounmask = True + if myoptions.changed_use is not False: myoptions.reinstall = "changed-use" myoptions.changed_use = False @@ -1604,7 +1615,7 @@ def emerge_main(): for line in textwrap.wrap(msg, 70): writemsg_level("!!! %s\n" % (line,), level=logging.ERROR, noiselevel=-1) - for i in e[0]: + for i in e.args[0]: writemsg_level(" %s\n" % colorize("INFORM", i), level=logging.ERROR, noiselevel=-1) writemsg_level("\n", level=logging.ERROR, noiselevel=-1) diff --git a/pym/_emerge/unmerge.py b/pym/_emerge/unmerge.py index c8ad7a2e..5b466e2d 100644 --- a/pym/_emerge/unmerge.py +++ b/pym/_emerge/unmerge.py @@ -40,9 +40,9 @@ def unmerge(root_config, myopts, unmerge_action, def _pkg(cpv): pkg = pkg_cache.get(cpv) if pkg is None: - pkg = Package(cpv=cpv, installed=True, + pkg = Package(built=True, cpv=cpv, installed=True, metadata=zip(db_keys, vartree.dbapi.aux_get(cpv, db_keys)), - root_config=root_config, + operation="uninstall", root_config=root_config, type_name="installed") pkg_cache[cpv] = pkg return pkg diff --git a/pym/portage/dbapi/vartree.py b/pym/portage/dbapi/vartree.py index b95d8c2e..dc8648b3 100644 --- a/pym/portage/dbapi/vartree.py +++ b/pym/portage/dbapi/vartree.py @@ -1878,14 +1878,14 @@ class dblink(object): """ import re - _normalize_needed = re.compile(r'.*//.*|^[^/]|.+/$|(^|.*/)\.\.?(/.*|$)') - _contents_split_counts = { - "dev": 2, - "dir": 2, - "fif": 2, - "obj": 4, - "sym": 5 - } + _normalize_needed = re.compile(r'//|^[^/]|./$|(^|/)\.\.?(/|$)') + + _contents_re = re.compile(r'^(' + \ + r'(?P<dir>(dev|dir|fif) (.+))|' + \ + r'(?P<obj>(obj) (.+) (\S+) (\d+))|' + \ + r'(?P<sym>(sym) (.+) -> (.+) (\d+))' + \ + r')$' + ) # When looping over files for merge/unmerge, temporarily yield to the # scheduler each time this many files are processed. @@ -2033,7 +2033,10 @@ class dblink(object): myc.close() null_byte = "\0" normalize_needed = self._normalize_needed - contents_split_counts = self._contents_split_counts + contents_re = self._contents_re + obj_index = contents_re.groupindex['obj'] + dir_index = contents_re.groupindex['dir'] + sym_index = contents_re.groupindex['sym'] myroot = self.myroot if myroot == os.path.sep: myroot = None @@ -2045,63 +2048,40 @@ class dblink(object): errors.append((pos + 1, _("Null byte found in CONTENTS entry"))) continue line = line.rstrip("\n") - # Split on " " so that even file paths that - # end with spaces can be handled. - mydat = line.split(" ") - entry_type = mydat[0] # empty string if line is empty - correct_split_count = contents_split_counts.get(entry_type) - if correct_split_count and len(mydat) > correct_split_count: - # Apparently file paths contain spaces, so reassemble - # the split have the correct_split_count. - newsplit = [entry_type] - spaces_total = len(mydat) - correct_split_count - if entry_type == "sym": - try: - splitter = mydat.index("->", 2, len(mydat) - 2) - except ValueError: - errors.append((pos + 1, _("Unrecognized CONTENTS entry"))) - continue - spaces_in_path = splitter - 2 - spaces_in_target = spaces_total - spaces_in_path - newsplit.append(" ".join(mydat[1:splitter])) - newsplit.append("->") - target_end = splitter + spaces_in_target + 2 - newsplit.append(" ".join(mydat[splitter + 1:target_end])) - newsplit.extend(mydat[target_end:]) - else: - path_end = spaces_total + 2 - newsplit.append(" ".join(mydat[1:path_end])) - newsplit.extend(mydat[path_end:]) - mydat = newsplit - - # we do this so we can remove from non-root filesystems - # (use the ROOT var to allow maintenance on other partitions) - try: - if normalize_needed.match(mydat[1]): - mydat[1] = normalize_path(mydat[1]) - if not mydat[1].startswith(os.path.sep): - mydat[1] = os.path.sep + mydat[1] - if myroot: - mydat[1] = os.path.join(myroot, mydat[1].lstrip(os.path.sep)) - if mydat[0] == "obj": - #format: type, mtime, md5sum - pkgfiles[mydat[1]] = [mydat[0], mydat[3], mydat[2]] - elif mydat[0] == "dir": - #format: type - pkgfiles[mydat[1]] = [mydat[0]] - elif mydat[0] == "sym": - #format: type, mtime, dest - pkgfiles[mydat[1]] = [mydat[0], mydat[4], mydat[3]] - elif mydat[0] == "dev": - #format: type - pkgfiles[mydat[1]] = [mydat[0]] - elif mydat[0]=="fif": - #format: type - pkgfiles[mydat[1]] = [mydat[0]] - else: - errors.append((pos + 1, _("Unrecognized CONTENTS entry"))) - except (KeyError, IndexError): + m = contents_re.match(line) + if m is None: errors.append((pos + 1, _("Unrecognized CONTENTS entry"))) + continue + + if m.group(obj_index) is not None: + base = obj_index + #format: type, mtime, md5sum + data = (m.group(base+1), m.group(base+4), m.group(base+3)) + elif m.group(dir_index) is not None: + base = dir_index + #format: type + data = (m.group(base+1),) + elif m.group(sym_index) is not None: + base = sym_index + #format: type, mtime, dest + data = (m.group(base+1), m.group(base+4), m.group(base+3)) + else: + # This won't happen as long the regular expression + # is written to only match valid entries. + raise AssertionError(_("required group not found " + \ + "in CONTENTS entry: '%s'") % line) + + path = m.group(base+2) + if normalize_needed.search(path) is not None: + path = normalize_path(path) + if not path.startswith(os.path.sep): + path = os.path.sep + path + + if myroot is not None: + path = os.path.join(myroot, path.lstrip(os.path.sep)) + + pkgfiles[path] = data + if errors: writemsg(_("!!! Parse error in '%s'\n") % contents_file, noiselevel=-1) for pos, e in errors: diff --git a/pym/portage/dep/__init__.py b/pym/portage/dep/__init__.py index bc77ec54..a048558a 100644 --- a/pym/portage/dep/__init__.py +++ b/pym/portage/dep/__init__.py @@ -519,9 +519,9 @@ class _use_dep(object): tokens.extend(x for x in self.enabled if x not in other_use) tokens.extend("-" + x for x in self.disabled if x in other_use) if conditional: - if not parent_use: + if parent_use is None: raise InvalidAtom("violated_conditionals needs 'parent_use'" + \ - " parameter for conditional flags: '%s'" % (token,)) + " parameter for conditional flags.") tokens.extend(x + "?" for x in conditional.enabled if x in parent_use and not x in other_use) tokens.extend("!" + x + "?" for x in conditional.disabled if x not in parent_use and x in other_use) tokens.extend(x + "=" for x in conditional.equal if x in parent_use and x not in other_use) @@ -791,6 +791,18 @@ class ExtendedAtomDict(portage.cache.mappings.MutableMapping): self._normal = {} self._value_class = value_class + def __iter__(self): + for k in self._normal: + yield k + for k in self._extended: + yield k + + if sys.hexversion >= 0x3000000: + keys = __iter__ + + def __len__(self): + return len(self._normal) + len(self._extended) + def setdefault(self, cp, default=None): if "*" in cp: return self._extended.setdefault(cp, default) diff --git a/pym/portage/dep/dep_check.py b/pym/portage/dep/dep_check.py index f4a44611..8747bb14 100644 --- a/pym/portage/dep/dep_check.py +++ b/pym/portage/dep/dep_check.py @@ -28,6 +28,7 @@ def _expand_new_virtuals(mysplit, edebug, mydbapi, mysettings, myroot="/", newsplit = [] mytrees = trees[myroot] portdb = mytrees["porttree"].dbapi + pkg_use_enabled = mytrees.get("pkg_use_enabled") atom_graph = mytrees.get("atom_graph") parent = mytrees.get("parent") virt_parent = mytrees.get("virt_parent") @@ -100,7 +101,8 @@ def _expand_new_virtuals(mysplit, edebug, mydbapi, mysettings, myroot="/", atom_graph.add(x, graph_parent) continue - if repoman or not hasattr(portdb, 'match_pkgs'): + if repoman or not hasattr(portdb, 'match_pkgs') or \ + pkg_use_enabled is None: if portdb.cp_list(x.cp): newsplit.append(x) else: @@ -149,7 +151,7 @@ def _expand_new_virtuals(mysplit, edebug, mydbapi, mysettings, myroot="/", # should enforce this. depstring = pkg.metadata['RDEPEND'] pkg_kwargs = kwargs.copy() - pkg_kwargs["myuse"] = pkg.use.enabled + pkg_kwargs["myuse"] = pkg_use_enabled(pkg) if edebug: writemsg_level(_("Virtual Parent: %s\n") \ % (pkg,), noiselevel=-1, level=logging.DEBUG) diff --git a/pym/portage/package/ebuild/config.py b/pym/portage/package/ebuild/config.py index 2ffad468..4f5f9413 100644 --- a/pym/portage/package/ebuild/config.py +++ b/pym/portage/package/ebuild/config.py @@ -843,12 +843,8 @@ class config(object): else: self.configdict["conf"]["ACCEPT_LICENSE"] = " ".join(v) for k, v in licdict.items(): - cp = k.cp - cp_dict = self._plicensedict.get(cp) - if not cp_dict: - cp_dict = {} - self._plicensedict[cp] = cp_dict - cp_dict[k] = self.expandLicenseTokens(v) + self._plicensedict.setdefault(k.cp, {})[k] = \ + self.expandLicenseTokens(v) #package.properties propdict = grabdict_package(os.path.join( @@ -860,12 +856,7 @@ class config(object): else: self.configdict["conf"]["ACCEPT_PROPERTIES"] = " ".join(v) for k, v in propdict.items(): - cp = k.cp - cp_dict = self._ppropertiesdict.get(cp) - if not cp_dict: - cp_dict = {} - self._ppropertiesdict[cp] = cp_dict - cp_dict[k] = v + self._ppropertiesdict.setdefault(k.cp, {})[k] = v #getting categories from an external file now categories = [grabfile(os.path.join(x, "categories")) for x in locations] @@ -1425,22 +1416,24 @@ class config(object): has_changed = True defaults = [] - pos = 0 for i, pkgprofileuse_dict in enumerate(self.pkgprofileuse): + if self.make_defaults_use[i]: + defaults.append(self.make_defaults_use[i]) cpdict = pkgprofileuse_dict.get(cp) if cpdict: + pkg_defaults = [] keys = list(cpdict) while keys: bestmatch = best_match_to_list(cpv_slot, keys) if bestmatch: keys.remove(bestmatch) - defaults.insert(pos, cpdict[bestmatch]) + pkg_defaults.append(cpdict[bestmatch]) else: break - del keys - if self.make_defaults_use[i]: - defaults.insert(pos, self.make_defaults_use[i]) - pos = len(defaults) + if pkg_defaults: + # reverse, so the most specific atoms come last + pkg_defaults.reverse() + defaults.extend(pkg_defaults) defaults = " ".join(defaults) if defaults != self.configdict["defaults"].get("USE",""): self.configdict["defaults"]["USE"] = defaults @@ -1614,22 +1607,24 @@ class config(object): if cp is None: cp = cpv_getkey(remove_slot(pkg)) usemask = [] - pos = 0 for i, pusemask_dict in enumerate(self.pusemask_list): + if self.usemask_list[i]: + usemask.append(self.usemask_list[i]) cpdict = pusemask_dict.get(cp) if cpdict: + pkg_usemask = [] keys = list(cpdict) while keys: best_match = best_match_to_list(pkg, keys) if best_match: keys.remove(best_match) - usemask.insert(pos, cpdict[best_match]) + pkg_usemask.append(cpdict[best_match]) else: break - del keys - if self.usemask_list[i]: - usemask.insert(pos, self.usemask_list[i]) - pos = len(usemask) + if pkg_usemask: + # reverse, so the most specific atoms come last + pkg_usemask.reverse() + usemask.extend(pkg_usemask) return set(stack_lists(usemask, incremental=True)) def _getUseForce(self, pkg): @@ -1637,22 +1632,24 @@ class config(object): if cp is None: cp = cpv_getkey(remove_slot(pkg)) useforce = [] - pos = 0 for i, puseforce_dict in enumerate(self.puseforce_list): + if self.useforce_list[i]: + useforce.append(self.useforce_list[i]) cpdict = puseforce_dict.get(cp) if cpdict: + pkg_useforce = [] keys = list(cpdict) while keys: best_match = best_match_to_list(pkg, keys) if best_match: keys.remove(best_match) - useforce.insert(pos, cpdict[best_match]) + pkg_useforce.append(cpdict[best_match]) else: break - del keys - if self.useforce_list[i]: - useforce.insert(pos, self.useforce_list[i]) - pos = len(useforce) + if pkg_useforce: + # reverse, so the most specific atoms come last + pkg_useforce.reverse() + useforce.extend(pkg_useforce) return set(stack_lists(useforce, incremental=True)) def _getMaskAtom(self, cpv, metadata): @@ -1721,19 +1718,22 @@ class config(object): if 'repository' in metadata: pkg = "".join((pkg, _repo_separator, metadata['repository'])) keywords = [[x for x in metadata["KEYWORDS"].split() if x != "-*"]] - pos = len(keywords) for pkeywords_dict in self._pkeywords_list: cpdict = pkeywords_dict.get(cp) if cpdict: + pkg_keywords = [] keys = list(cpdict) while keys: best_match = best_match_to_list(pkg, keys) if best_match: keys.remove(best_match) - keywords.insert(pos, cpdict[best_match]) + pkg_keywords.append(cpdict[best_match]) else: break - pos = len(keywords) + if pkg_keywords: + # reverse, so the most specific atoms come last + pkg_keywords.reverse() + keywords.extend(pkg_keywords) return stack_lists(keywords, incremental=True) def _getMissingKeywords(self, cpv, metadata): @@ -1765,14 +1765,24 @@ class config(object): pkgdict = self.pkeywordsdict.get(cp) matches = False if pkgdict: - pkg = "".join((cpv, _slot_separator, metadata["SLOT"])) + cpv_slot = "%s:%s" % (cpv, metadata["SLOT"]) if 'repository' in metadata: - pkg = "".join((pkg, _repo_separator, metadata['repository'])) - cpv_slot_list = [pkg] - for atom, pkgkeywords in pkgdict.items(): - if match_from_list(atom, cpv_slot_list): - matches = True - pgroups.extend(pkgkeywords) + cpv_slot = "".join((cpv_slot, _repo_separator, metadata['repository'])) + pkg_accept_keywords = [] + keys = list(pkgdict) + while keys: + best_match = best_match_to_list(cpv_slot, keys) + if best_match: + keys.remove(best_match) + pkg_accept_keywords.append(pkgdict[best_match]) + else: + break + if pkg_accept_keywords: + # reverse, so the most specific atoms come last + pkg_accept_keywords.reverse() + for x in pkg_accept_keywords: + pgroups.extend(x) + matches = True if matches or egroups: pgroups.extend(egroups) inc_pgroups = set() @@ -1834,12 +1844,24 @@ class config(object): cp = cpv_getkey(cpv) cpdict = self._plicensedict.get(cp) if cpdict: - accept_license = list(self._accept_license) - cpv_slot = "".join((cpv, _slot_separator, metadata["SLOT"])) + cpv_slot = "%s:%s" % (cpv, metadata["SLOT"]) if 'repository' in metadata: cpv_slot = "".join((cpv_slot, _repo_separator, metadata['repository'])) - for atom in match_to_list(cpv_slot, list(cpdict)): - accept_license.extend(cpdict[atom]) + keys = list(cpdict) + plicence_list = [] + while keys: + bestmatch = best_match_to_list(cpv_slot, keys) + if bestmatch: + keys.remove(bestmatch) + plicence_list.append(cpdict[bestmatch]) + else: + break + if plicence_list: + # reverse, so the most specific atoms come last + plicence_list.reverse() + accept_license = list(self._accept_license) + for x in plicence_list: + accept_license.extend(x) licenses = set(flatten(use_reduce(paren_reduce( metadata["LICENSE"]), matchall=1))) @@ -1916,12 +1938,24 @@ class config(object): cp = cpv_getkey(cpv) cpdict = self._ppropertiesdict.get(cp) if cpdict: - accept_properties = list(self._accept_properties) - cpv_slot = "".join((cpv, _slot_separator, metadata["SLOT"])) + cpv_slot = "%s:%s" % (cpv, metadata["SLOT"]) if 'repository' in metadata: cpv_slot = "".join((cpv_slot, _repo_separator, metadata['repository'])) - for atom in match_to_list(cpv_slot, list(cpdict)): - accept_properties.extend(cpdict[atom]) + keys = list(cpdict) + pproperties_list = [] + while keys: + bestmatch = best_match_to_list(cpv_slot, keys) + if bestmatch: + keys.remove(bestmatch) + pproperties_list.append(cpdict[bestmatch]) + else: + break + if pproperties_list: + # reverse, so the most specific atoms come last + pproperties_list.reverse() + accept_properties = list(self._accept_properties) + for x in pproperties_list: + accept_properties.extend(x) properties = set(flatten(use_reduce(paren_reduce( metadata["PROPERTIES"]), matchall=1))) @@ -2228,6 +2262,9 @@ class config(object): # For optimal performance, use slice # comparison instead of startswith(). + iuse = self.configdict["pkg"].get("IUSE") + if iuse is not None: + iuse = [x.lstrip("+-") for x in iuse.split()] myflags = set() for curdb in self.uvlist: cur_use_expand = [x for x in use_expand if x in curdb] @@ -2247,10 +2284,33 @@ class config(object): continue if x[0] == "-": + if x[-2:] == '_*': + prefix = x[1:-1] + prefix_len = len(prefix) + myflags.difference_update( + [y for y in myflags if \ + y[:prefix_len] == prefix]) myflags.discard(x[1:]) continue - myflags.add(x) + if iuse is not None and x[-2:] == '_*': + # Expand wildcards here, so that cases like + # USE="linguas_* -linguas_en_US" work correctly. + prefix = x[:-1] + prefix_len = len(prefix) + has_iuse = False + for y in iuse: + if y[:prefix_len] == prefix: + has_iuse = True + myflags.add(y) + if not has_iuse: + # There are no matching IUSE, so allow the + # wildcard to pass through. This allows + # linguas_* to trigger unset LINGUAS in + # cases when no linguas_ flags are in IUSE. + myflags.add(x) + else: + myflags.add(x) for var in cur_use_expand: var_lower = var.lower() diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py index 78d1360c..afffd45d 100644 --- a/pym/portage/package/ebuild/doebuild.py +++ b/pym/portage/package/ebuild/doebuild.py @@ -1268,7 +1268,7 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero buf = array.array('B') try: buf.fromfile(f, buffsize) - except EOFError: + except (EOFError, IOError): pass if not buf: eof = True diff --git a/pym/portage/package/ebuild/getmaskingstatus.py b/pym/portage/package/ebuild/getmaskingstatus.py index 8fbbff02..5e5a13f1 100644 --- a/pym/portage/package/ebuild/getmaskingstatus.py +++ b/pym/portage/package/ebuild/getmaskingstatus.py @@ -15,12 +15,26 @@ from portage.versions import catpkgsplit, cpv_getkey if sys.hexversion >= 0x3000000: basestring = str +class _MaskReason(object): + + __slots__ = ('category', 'message', 'hint') + + def __init__(self, category, message, hint=None): + self.category = category + self.message = message + self.hint = hint + def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): if settings is None: settings = config(clone=portage.settings) if portdb is None: portdb = portage.portdb + return [mreason.message for \ + mreason in _getmaskingstatus(mycpv, settings, portdb,myrepo)] + +def _getmaskingstatus(mycpv, settings, portdb, myrepo=None): + metadata = None installed = False if not isinstance(mycpv, basestring): @@ -40,7 +54,7 @@ def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): except KeyError: if not portdb.cpv_exists(mycpv): raise - return ["corruption"] + return [_MaskReason("corruption", "corruption")] if "?" in metadata["LICENSE"]: settings.setcpv(mycpv, mydb=metadata) metadata["USE"] = settings["PORTAGE_USE"] @@ -51,11 +65,11 @@ def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): # profile checking if settings._getProfileMaskAtom(mycpv, metadata): - rValue.append("profile") + rValue.append(_MaskReason("profile", "profile")) # package.mask checking if settings._getMaskAtom(mycpv, metadata): - rValue.append("package.mask") + rValue.append(_MaskReason("package.mask", "package.mask")) # keywords checking eapi = metadata["EAPI"] @@ -65,9 +79,9 @@ def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): if eapi.startswith("-"): eapi = eapi[1:] if not eapi_is_supported(eapi): - return ["EAPI %s" % eapi] + return [_MaskReason("EAPI", "EAPI %s" % eapi)] elif _eapi_is_deprecated(eapi) and not installed: - return ["EAPI %s" % eapi] + return [_MaskReason("EAPI", "EAPI %s" % eapi)] egroups = settings.configdict["backupenv"].get( "ACCEPT_KEYWORDS", "").split() pgroups = settings["ACCEPT_KEYWORDS"].split() @@ -104,6 +118,7 @@ def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): del inc_pgroups kmask = "missing" + kmask_hint = None if '**' in pgroups: kmask = None @@ -123,6 +138,7 @@ def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): break elif gp=="~"+myarch and myarch in pgroups: kmask="~"+myarch + kmask_hint = "unstable keyword" break try: @@ -135,9 +151,9 @@ def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): if x in allowed_tokens] msg = license_split[:] msg.append("license(s)") - rValue.append(" ".join(msg)) + rValue.append(_MaskReason("LICENSE", " ".join(msg))) except portage.exception.InvalidDependString as e: - rValue.append("LICENSE: "+str(e)) + rValue.append(_MaskReason("invalid", "LICENSE: "+str(e))) try: missing_properties = settings._getMissingProperties(mycpv, metadata) @@ -149,13 +165,14 @@ def getmaskingstatus(mycpv, settings=None, portdb=None, myrepo=None): if x in allowed_tokens] msg = properties_split[:] msg.append("properties") - rValue.append(" ".join(msg)) + rValue.append(_MaskReason("PROPERTIES", " ".join(msg))) except portage.exception.InvalidDependString as e: - rValue.append("PROPERTIES: "+str(e)) + rValue.append(_MaskReason("invalid", "PROPERTIES: "+str(e))) # Only show KEYWORDS masks for installed packages # if they're not masked for any other reason. if kmask and (not installed or not rValue): - rValue.append(kmask+" keyword") + rValue.append(_MaskReason("KEYWORDS", + kmask + " keyword", hint=kmask_hint)) return rValue diff --git a/pym/portage/tests/__init__.py b/pym/portage/tests/__init__.py index 393ecf78..bd41f1ee 100644 --- a/pym/portage/tests/__init__.py +++ b/pym/portage/tests/__init__.py @@ -26,6 +26,10 @@ def main(): basedir = os.path.dirname(os.path.realpath(__file__)) testDirs = [] + if len(sys.argv) > 1: + suite.addTests(getTestFromCommandLine(sys.argv[1:], basedir)) + return TextTestRunner(verbosity=2).run(suite) + # the os.walk help mentions relative paths as being quirky # I was tired of adding dirs to the list, so now we add __test__ # to each dir we want tested. @@ -52,6 +56,29 @@ def my_import(name): mod = getattr(mod, comp) return mod +def getTestFromCommandLine(args, base_path): + ret = [] + for arg in args: + realpath = os.path.realpath(arg) + path = os.path.dirname(realpath) + f = realpath[len(path)+1:] + + if not f.startswith("test") or not f.endswith(".py"): + raise Exception("Invalid argument: '%s'" % arg) + + mymodule = f[:-3] + + parent_path = path[len(base_path)+1:] + parent_module = ".".join(("portage", "tests", parent_path)) + parent_module = parent_module.replace('/', '.') + result = [] + + # Make the trailing / a . for module importing + modname = ".".join((parent_module, mymodule)) + mod = my_import(modname) + ret.append(unittest.TestLoader().loadTestsFromModule(mod)) + return ret + def getTests(path, base_path): """ diff --git a/pym/portage/tests/dep/testAtom.py b/pym/portage/tests/dep/testAtom.py index 9345711f..14a4e0f6 100644 --- a/pym/portage/tests/dep/testAtom.py +++ b/pym/portage/tests/dep/testAtom.py @@ -96,3 +96,53 @@ class TestAtom(TestCase): for atom, allow_wildcard in tests_xfail: self.assertRaisesMsg(atom, portage.exception.InvalidAtom, Atom, atom) + + def test_violated_conditionals(self): + test_cases = ( + ("dev-libs/A", ["foo"], None, "dev-libs/A"), + ("dev-libs/A[foo]", [], None, "dev-libs/A[foo]"), + ("dev-libs/A[foo]", ["foo"], None, "dev-libs/A"), + ("dev-libs/A[foo]", [], [], "dev-libs/A[foo]"), + ("dev-libs/A[foo]", ["foo"], [], "dev-libs/A"), + + ("dev-libs/A:0[foo]", ["foo"], [], "dev-libs/A:0"), + + ("dev-libs/A[foo,-bar]", [], None, "dev-libs/A[foo]"), + ("dev-libs/A[-foo,bar]", [], None, "dev-libs/A[bar]"), + + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", [], [], "dev-libs/A[a,!c=]"), + + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["a"], [], "dev-libs/A[!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["b"], [], "dev-libs/A[a,b=,!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["c"], [], "dev-libs/A[a]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["d"], [], "dev-libs/A[a,!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["e"], [], "dev-libs/A[a,!e?,!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["f"], [], "dev-libs/A[a,-f,!c=]"), + + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["a"], ["a"], "dev-libs/A[!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["b"], ["b"], "dev-libs/A[a,!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["c"], ["c"], "dev-libs/A[a,!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["d"], ["d"], "dev-libs/A[a,!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["e"], ["e"], "dev-libs/A[a,!c=]"), + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", ["f"], ["f"], "dev-libs/A[a,-f,!c=]"), + ) + + test_cases_xfail = ( + ("dev-libs/A[a,b=,!c=,d?,!e?,-f]", [], None), + ) + + for atom, other_use, parent_use, expected_violated_atom in test_cases: + a = Atom(atom) + violated_atom = a.violated_conditionals(other_use, parent_use) + if parent_use is None: + fail_msg = "Atom: %s, other_use: %s, parent_use: %s, got: %s, expected: %s" % \ + (atom, " ".join(other_use), "None", str(violated_atom), expected_violated_atom) + else: + fail_msg = "Atom: %s, other_use: %s, parent_use: %s, got: %s, expected: %s" % \ + (atom, " ".join(other_use), " ".join(parent_use), str(violated_atom), expected_violated_atom) + self.assertEqual(str(violated_atom), expected_violated_atom, fail_msg) + + for atom, other_use, parent_use in test_cases_xfail: + a = Atom(atom) + self.assertRaisesMsg(atom, portage.exception.InvalidAtom, \ + a.violated_conditionals, other_use, parent_use) diff --git a/pym/portage/tests/ebuild/test_array_fromfile_eof.py b/pym/portage/tests/ebuild/test_array_fromfile_eof.py index 3f2a6c7c..d8277f27 100644 --- a/pym/portage/tests/ebuild/test_array_fromfile_eof.py +++ b/pym/portage/tests/ebuild/test_array_fromfile_eof.py @@ -28,7 +28,7 @@ class ArrayFromfileEofTestCase(TestCase): a = array.array('B') try: a.fromfile(f, len(input_bytes) + 1) - except EOFError: + except (EOFError, IOError): # python-3.0 lost data here eof = True diff --git a/pym/portage/tests/ebuild/test_spawn.py b/pym/portage/tests/ebuild/test_spawn.py index f37a0c5a..bb9fb262 100644 --- a/pym/portage/tests/ebuild/test_spawn.py +++ b/pym/portage/tests/ebuild/test_spawn.py @@ -4,16 +4,19 @@ import codecs import errno import sys +from tempfile import mkstemp from portage import os from portage import _encodings from portage import _unicode_encode +from portage import spawn from portage.tests import TestCase +from portage.tests.resolver.ResolverPlayground import ResolverPlayground class SpawnTestCase(TestCase): def testLogfile(self): - from portage import settings, spawn - from tempfile import mkstemp + playground = ResolverPlayground() + settings = playground.settings logfile = None try: fd, logfile = mkstemp() @@ -42,6 +45,7 @@ class SpawnTestCase(TestCase): # may occur. self.assertEqual(test_string, log_content) finally: + playground.cleanup() if logfile: try: os.unlink(logfile) diff --git a/pym/portage/tests/lazyimport/__init__.py b/pym/portage/tests/lazyimport/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pym/portage/tests/lazyimport/__init__.py diff --git a/pym/portage/tests/lazyimport/__test__ b/pym/portage/tests/lazyimport/__test__ new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pym/portage/tests/lazyimport/__test__ diff --git a/pym/portage/tests/lazyimport/test_preload_portage_submodules.py b/pym/portage/tests/lazyimport/test_preload_portage_submodules.py new file mode 100644 index 00000000..9d20ebac --- /dev/null +++ b/pym/portage/tests/lazyimport/test_preload_portage_submodules.py @@ -0,0 +1,16 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import portage +from portage.tests import TestCase + +class PreloadPortageSubmodulesTestCase(TestCase): + + def testPreloadPortageSubmodules(self): + """ + Verify that _preload_portage_submodules() doesn't leave any + remaining proxies that refer to the portage.* namespace. + """ + portage.proxy.lazyimport._preload_portage_submodules() + for name in portage.proxy.lazyimport._module_proxies: + self.assertEqual(name.startswith('portage.'), False) diff --git a/pym/portage/tests/resolver/ResolverPlayground.py b/pym/portage/tests/resolver/ResolverPlayground.py new file mode 100644 index 00000000..b4ff4180 --- /dev/null +++ b/pym/portage/tests/resolver/ResolverPlayground.py @@ -0,0 +1,227 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +from itertools import chain +import shutil +import tempfile +import portage +from portage import os +from portage.dbapi.vartree import vartree +from portage.dbapi.porttree import portagetree +from portage.dbapi.bintree import binarytree +from portage.dep import Atom +from portage.package.ebuild.config import config +from portage.sets import load_default_config +from portage.versions import catsplit + +from _emerge.Blocker import Blocker +from _emerge.create_depgraph_params import create_depgraph_params +from _emerge.depgraph import backtrack_depgraph +from _emerge.RootConfig import RootConfig + +class ResolverPlayground(object): + """ + This class help to create the necessary files on disk and + the needed settings instances, etc. for the resolver to do + it's work. + """ + + def __init__(self, ebuilds={}, installed={}, profile={}): + """ + ebuilds: cpv -> metadata mapping simulating avaiable ebuilds. + installed: cpv -> metadata mapping simulating installed packages. + If a metadata key is missing, it gets a default value. + profile: settings defined by the profile. + """ + self.root = tempfile.mkdtemp() + os.path.sep + self.portdir = os.path.join(self.root, "usr/portage") + self.vdbdir = os.path.join(self.root, "var/db/pkg") + os.makedirs(self.portdir) + os.makedirs(self.vdbdir) + + self._create_ebuilds(ebuilds) + self._create_installed(installed) + self._create_profile(ebuilds, installed, profile) + + self.settings, self.trees = self._load_config() + + self._create_ebuild_manifests(ebuilds) + + def _create_ebuilds(self, ebuilds): + for cpv in ebuilds: + a = Atom("=" + cpv) + ebuild_dir = os.path.join(self.portdir, a.cp) + ebuild_path = os.path.join(ebuild_dir, a.cpv.split("/")[1] + ".ebuild") + try: + os.makedirs(ebuild_dir) + except os.error: + pass + + metadata = ebuilds[cpv] + eapi = metadata.get("EAPI", 0) + slot = metadata.get("SLOT", 0) + keywords = metadata.get("KEYWORDS", "x86") + iuse = metadata.get("IUSE", "") + depend = metadata.get("DEPEND", "") + rdepend = metadata.get("RDEPEND", None) + pdepend = metadata.get("PDEPEND", None) + + f = open(ebuild_path, "w") + f.write('EAPI="' + str(eapi) + '"\n') + f.write('SLOT="' + str(slot) + '"\n') + f.write('KEYWORDS="' + str(keywords) + '"\n') + f.write('IUSE="' + str(iuse) + '"\n') + f.write('DEPEND="' + str(depend) + '"\n') + if rdepend is not None: + f.write('RDEPEND="' + str(rdepend) + '"\n') + if rdepend is not None: + f.write('PDEPEND="' + str(pdepend) + '"\n') + f.close() + + def _create_ebuild_manifests(self, ebuilds): + for cpv in ebuilds: + a = Atom("=" + cpv) + ebuild_dir = os.path.join(self.portdir, a.cp) + ebuild_path = os.path.join(ebuild_dir, a.cpv.split("/")[1] + ".ebuild") + + portage.util.noiselimit = -1 + tmpsettings = config(clone=self.settings) + portdb = self.trees[self.root]["porttree"].dbapi + portage.doebuild(ebuild_path, "digest", self.root, tmpsettings, + tree="porttree", mydbapi=portdb) + portage.util.noiselimit = 0 + + def _create_installed(self, installed): + for cpv in installed: + a = Atom("=" + cpv) + vdb_pkg_dir = os.path.join(self.vdbdir, a.cpv) + try: + os.makedirs(vdb_pkg_dir) + except os.error: + pass + + metadata = installed[cpv] + eapi = metadata.get("EAPI", 0) + slot = metadata.get("SLOT", 0) + keywords = metadata.get("KEYWORDS", "~x86") + iuse = metadata.get("IUSE", "") + use = metadata.get("USE", "") + depend = metadata.get("DEPEND", "") + rdepend = metadata.get("RDEPEND", None) + pdepend = metadata.get("PDEPEND", None) + + def write_key(key, value): + f = open(os.path.join(vdb_pkg_dir, key), "w") + f.write(str(value) + "\n") + f.close() + + write_key("EAPI", eapi) + write_key("SLOT", slot) + write_key("KEYWORDS", keywords) + write_key("IUSE", iuse) + write_key("USE", use) + write_key("DEPEND", depend) + if rdepend is not None: + write_key("RDEPEND", rdepend) + if rdepend is not None: + write_key("PDEPEND", pdepend) + + def _create_profile(self, ebuilds, installed, profile): + #Create $PORTDIR/profiles/categories + categories = set() + for cpv in chain(ebuilds.keys(), installed.keys()): + categories.add(catsplit(cpv)[0]) + + profile_dir = os.path.join(self.portdir, "profiles") + try: + os.makedirs(profile_dir) + except os.error: + pass + + categories_file = os.path.join(profile_dir, "categories") + + f = open(categories_file, "w") + for cat in categories: + f.write(cat + "\n") + f.close() + + #Create $PORTDIR/eclass (we fail to digest the ebuilds if it's not there) + os.makedirs(os.path.join(self.portdir, "eclass")) + + if profile: + #This is meant to allow the consumer to set up his own profile, + #with package.mask and what not. + raise NotImplentedError() + + def _load_config(self): + env = { "PORTDIR": self.portdir, "ROOT": self.root, "ACCEPT_KEYWORDS": "x86"} + settings = config(config_root=self.root, target_root=self.root, local_config=False, env=env) + settings.lock() + + trees = { + self.root: { + "virtuals": settings.getvirtuals(), + "vartree": vartree(self.root, categories=settings.categories, settings=settings), + "porttree": portagetree(self.root, settings=settings), + "bintree": binarytree(self.root, os.path.join(self.root, "usr/portage/packages"), settings=settings) + } + } + + for root, root_trees in trees.items(): + settings = root_trees["vartree"].settings + settings._init_dirs() + setconfig = load_default_config(settings, root_trees) + root_trees["root_config"] = RootConfig(settings, root_trees, setconfig) + + return settings, trees + + def run(self, myfiles, myopts={}, myaction=None): + myopts["--pretend"] = True + myopts["--quiet"] = True + myopts["--root"] = self.root + myopts["--config-root"] = self.root + myopts["--root-deps"] = "rdeps" + # Add a fake _test_ option that can be used for + # conditional test code. + myopts["_test_"] = True + + portage.util.noiselimit = -2 + myparams = create_depgraph_params(myopts, myaction) + success, mydepgraph, favorites = backtrack_depgraph( + self.settings, self.trees, myopts, myparams, myaction, myfiles, None) + result = ResolverPlaygroundResult(success, mydepgraph, favorites) + portage.util.noiselimit = 0 + + return result + + def cleanup(self): + shutil.rmtree(self.root) + +class ResolverPlaygroundResult(object): + def __init__(self, success, mydepgraph, favorites): + self.success = success + self.depgraph = mydepgraph + self.favorites = favorites + self.mergelist = None + self.use_changes = None + self.unstable_keywords = None + + if self.depgraph._dynamic_config._serialized_tasks_cache is not None: + self.mergelist = [] + for x in self.depgraph._dynamic_config._serialized_tasks_cache: + if isinstance(x, Blocker): + self.mergelist.append(x.atom) + else: + self.mergelist.append(x.cpv) + + if self.depgraph._dynamic_config._needed_use_config_changes: + self.use_changes = {} + for pkg, needed_use_config_changes in \ + self.depgraph._dynamic_config._needed_use_config_changes.items(): + new_use, changes = needed_use_config_changes + self.use_changes[pkg.cpv] = changes + + if self.depgraph._dynamic_config._needed_unstable_keywords: + self.unstable_keywords = set() + for pkg in self.depgraph._dynamic_config._needed_unstable_keywords: + self.unstable_keywords.add(pkg.cpv) diff --git a/pym/portage/tests/resolver/__init__.py b/pym/portage/tests/resolver/__init__.py new file mode 100644 index 00000000..21a391ae --- /dev/null +++ b/pym/portage/tests/resolver/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 diff --git a/pym/portage/tests/resolver/__test__ b/pym/portage/tests/resolver/__test__ new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pym/portage/tests/resolver/__test__ diff --git a/pym/portage/tests/resolver/test_autounmask.py b/pym/portage/tests/resolver/test_autounmask.py new file mode 100644 index 00000000..d528f907 --- /dev/null +++ b/pym/portage/tests/resolver/test_autounmask.py @@ -0,0 +1,116 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +from portage.tests import TestCase +from portage.tests.resolver.ResolverPlayground import ResolverPlayground + +class AutounmaskTestCase(TestCase): + + def testAutounmask(self): + ebuilds = { + #ebuilds to test use changes + "dev-libs/A-1": { "SLOT": 1, "DEPEND": "dev-libs/B[foo]", "EAPI": 2}, + "dev-libs/A-2": { "SLOT": 2, "DEPEND": "dev-libs/B[bar]", "EAPI": 2}, + "dev-libs/B-1": { "DEPEND": "foo? ( dev-libs/C ) bar? ( dev-libs/D )", "IUSE": "foo bar"}, + "dev-libs/C-1": {}, + "dev-libs/D-1": {}, + + #ebuilds to test keyword changes + "app-misc/Z-1": { "KEYWORDS": "~x86", "DEPEND": "app-misc/Y" }, + "app-misc/Y-1": { "KEYWORDS": "~x86" }, + "app-misc/W-1": {}, + "app-misc/W-2": { "KEYWORDS": "~x86" }, + "app-misc/V-1": { "KEYWORDS": "~x86", "DEPEND": ">=app-misc/W-2"}, + + #ebuilds for mixed test for || dep handling + "sci-libs/K-1": { "DEPEND": " || ( sci-libs/L[bar] || ( sci-libs/M sci-libs/P ) )", "EAPI": 2}, + "sci-libs/K-2": { "DEPEND": " || ( sci-libs/L[bar] || ( sci-libs/P sci-libs/M ) )", "EAPI": 2}, + "sci-libs/K-3": { "DEPEND": " || ( sci-libs/M || ( sci-libs/L[bar] sci-libs/P ) )", "EAPI": 2}, + "sci-libs/K-4": { "DEPEND": " || ( sci-libs/M || ( sci-libs/P sci-libs/L[bar] ) )", "EAPI": 2}, + "sci-libs/K-5": { "DEPEND": " || ( sci-libs/P || ( sci-libs/L[bar] sci-libs/M ) )", "EAPI": 2}, + "sci-libs/K-6": { "DEPEND": " || ( sci-libs/P || ( sci-libs/M sci-libs/L[bar] ) )", "EAPI": 2}, + "sci-libs/K-7": { "DEPEND": " || ( sci-libs/M sci-libs/L[bar] )", "EAPI": 2}, + "sci-libs/K-8": { "DEPEND": " || ( sci-libs/L[bar] sci-libs/M )", "EAPI": 2}, + + "sci-libs/L-1": { "IUSE": "bar" }, + "sci-libs/M-1": { "KEYWORDS": "~x86" }, + "sci-libs/P-1": { }, + } + + requests = ( + #Test USE changes. + #The simple case. + + (["dev-libs/A:1"], {"--autounmask": "n"}, None, False, None, None, None), + (["dev-libs/A:1"], {"--autounmask": True}, None, False, \ + ["dev-libs/C-1", "dev-libs/B-1", "dev-libs/A-1"], { "dev-libs/B-1": {"foo": True} }, None), + + #Make sure we restart if needed. + (["dev-libs/B", "dev-libs/A:1"], {"--autounmask": True}, None, False, \ + ["dev-libs/C-1", "dev-libs/B-1", "dev-libs/A-1"], { "dev-libs/B-1": {"foo": True} }, None), + (["dev-libs/A:1", "dev-libs/B"], {"--autounmask": True}, None, False, \ + ["dev-libs/C-1", "dev-libs/B-1", "dev-libs/A-1"], { "dev-libs/B-1": {"foo": True} }, None), + (["dev-libs/A:1", "dev-libs/A:2"], {"--autounmask": True}, None, False, \ + ["dev-libs/D-1", "dev-libs/C-1", "dev-libs/B-1", "dev-libs/A-1", "dev-libs/A-2"], \ + { "dev-libs/B-1": {"foo": True, "bar": True} }, None), + (["dev-libs/B", "dev-libs/A:1", "dev-libs/A:2"], {"--autounmask": True}, None, False, \ + ["dev-libs/D-1", "dev-libs/C-1", "dev-libs/B-1", "dev-libs/A-1", "dev-libs/A-2"], \ + { "dev-libs/B-1": {"foo": True, "bar": True} }, None), + (["dev-libs/A:1", "dev-libs/B", "dev-libs/A:2"], {"--autounmask": True}, None, False, \ + ["dev-libs/D-1", "dev-libs/C-1", "dev-libs/B-1", "dev-libs/A-1", "dev-libs/A-2"], \ + { "dev-libs/B-1": {"foo": True, "bar": True} }, None), + (["dev-libs/A:1", "dev-libs/A:2", "dev-libs/B"], {"--autounmask": True}, None, False, \ + ["dev-libs/D-1", "dev-libs/C-1", "dev-libs/B-1", "dev-libs/A-1", "dev-libs/A-2"], \ + { "dev-libs/B-1": {"foo": True, "bar": True} }, None), + + #Test keywording. + #The simple case. + + (["app-misc/Z"], {"--autounmask": "n"}, None, False, None, None, None), + (["app-misc/Z"], {"--autounmask": True}, None, False, \ + ["app-misc/Y-1", "app-misc/Z-1"], None, ["app-misc/Y-1", "app-misc/Z-1"]), + + #Make sure that the backtracking for slot conflicts handles our mess. + + (["=app-misc/V-1", "app-misc/W"], {"--autounmask": True}, None, False, \ + ["app-misc/W-2", "app-misc/V-1"], None, ["app-misc/W-2", "app-misc/V-1"]), + (["app-misc/W", "=app-misc/V-1"], {"--autounmask": True}, None, False, \ + ["app-misc/W-2", "app-misc/V-1"], None, ["app-misc/W-2", "app-misc/V-1"]), + + #Mixed testing + #Make sure we don't change use for something in a || dep if there is another choice + #that needs no change. + + (["=sci-libs/K-1"], {"--autounmask": True}, None, True, \ + ["sci-libs/P-1", "sci-libs/K-1"], None, None), + (["=sci-libs/K-2"], {"--autounmask": True}, None, True, \ + ["sci-libs/P-1", "sci-libs/K-2"], None, None), + (["=sci-libs/K-3"], {"--autounmask": True}, None, True, \ + ["sci-libs/P-1", "sci-libs/K-3"], None, None), + (["=sci-libs/K-4"], {"--autounmask": True}, None, True, \ + ["sci-libs/P-1", "sci-libs/K-4"], None, None), + (["=sci-libs/K-5"], {"--autounmask": True}, None, True, \ + ["sci-libs/P-1", "sci-libs/K-5"], None, None), + (["=sci-libs/K-6"], {"--autounmask": True}, None, True, \ + ["sci-libs/P-1", "sci-libs/K-6"], None, None), + + #Make sure we prefer use changes over keyword changes. + (["=sci-libs/K-7"], {"--autounmask": True}, None, False, \ + ["sci-libs/L-1", "sci-libs/K-7"], { "sci-libs/L-1": { "bar": True } }, None), + (["=sci-libs/K-8"], {"--autounmask": True}, None, False, \ + ["sci-libs/L-1", "sci-libs/K-8"], { "sci-libs/L-1": { "bar": True } }, None), + ) + + playground = ResolverPlayground(ebuilds=ebuilds) + try: + for atoms, options, action, expected_result, expected_mergelist, \ + expected_use_changes, expected_unstable_keywords in requests: + result = playground.run(atoms, options, action) + if expected_unstable_keywords is not None: + expected_unstable_keywords = set(expected_unstable_keywords) + self.assertEqual( + (result.success, result.mergelist, result.use_changes, result.unstable_keywords), + (expected_result, expected_mergelist, expected_use_changes, expected_unstable_keywords) + ) + finally: + playground.cleanup() diff --git a/pym/portage/tests/resolver/test_eapi.py b/pym/portage/tests/resolver/test_eapi.py new file mode 100644 index 00000000..d9f34f43 --- /dev/null +++ b/pym/portage/tests/resolver/test_eapi.py @@ -0,0 +1,102 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +from portage.tests import TestCase +from portage.tests.resolver.ResolverPlayground import ResolverPlayground + +class EAPITestCase(TestCase): + + def testEAPI(self): + ebuilds = { + #EAPI-1: IUSE-defaults + "dev-libs/A-1.0": { "EAPI": 0, "IUSE": "+foo" }, + "dev-libs/A-1.1": { "EAPI": 1, "IUSE": "+foo" }, + "dev-libs/A-1.2": { "EAPI": 2, "IUSE": "+foo" }, + "dev-libs/A-1.3": { "EAPI": 3, "IUSE": "+foo" }, + #~ "dev-libs/A-1.4": { "EAPI": 4, "IUSE": "+foo" }, + + #EAPI-1: slot deps + "dev-libs/A-2.0": { "EAPI": 0, "DEPEND": "dev-libs/B:0" }, + "dev-libs/A-2.1": { "EAPI": 1, "DEPEND": "dev-libs/B:0" }, + "dev-libs/A-2.2": { "EAPI": 2, "DEPEND": "dev-libs/B:0" }, + "dev-libs/A-2.3": { "EAPI": 3, "DEPEND": "dev-libs/B:0" }, + #~ "dev-libs/A-2.4": { "EAPI": 4, "DEPEND": "dev-libs/B:0" }, + + #EAPI-2: use deps + "dev-libs/A-3.0": { "EAPI": 0, "DEPEND": "dev-libs/B[foo]" }, + "dev-libs/A-3.1": { "EAPI": 1, "DEPEND": "dev-libs/B[foo]" }, + "dev-libs/A-3.2": { "EAPI": 2, "DEPEND": "dev-libs/B[foo]" }, + "dev-libs/A-3.3": { "EAPI": 3, "DEPEND": "dev-libs/B[foo]" }, + #~ "dev-libs/A-3.4": { "EAPI": 4, "DEPEND": "dev-libs/B[foo]" }, + + #EAPI-2: strong blocks + "dev-libs/A-4.0": { "EAPI": 0, "DEPEND": "!!dev-libs/B" }, + "dev-libs/A-4.1": { "EAPI": 1, "DEPEND": "!!dev-libs/B" }, + "dev-libs/A-4.2": { "EAPI": 2, "DEPEND": "!!dev-libs/B" }, + "dev-libs/A-4.3": { "EAPI": 3, "DEPEND": "!!dev-libs/B" }, + #~ "dev-libs/A-4.4": { "EAPI": 4, "DEPEND": "!!dev-libs/B" }, + + #EAPI-4: slot operator deps + #~ "dev-libs/A-5.0": { "EAPI": 0, "DEPEND": "dev-libs/B:*" }, + #~ "dev-libs/A-5.1": { "EAPI": 1, "DEPEND": "dev-libs/B:*" }, + #~ "dev-libs/A-5.2": { "EAPI": 2, "DEPEND": "dev-libs/B:*" }, + #~ "dev-libs/A-5.3": { "EAPI": 3, "DEPEND": "dev-libs/B:*" }, + #~ "dev-libs/A-5.4": { "EAPI": 4, "DEPEND": "dev-libs/B:*" }, + + #EAPI-4: slot operator deps + #~ "dev-libs/A-6.0": { "EAPI": 0, "DEPEND": "dev-libs/B[bar(+)]" }, + #~ "dev-libs/A-6.1": { "EAPI": 1, "DEPEND": "dev-libs/B[bar(+)]" }, + #~ "dev-libs/A-6.2": { "EAPI": 2, "DEPEND": "dev-libs/B[bar(+)]" }, + #~ "dev-libs/A-6.3": { "EAPI": 3, "DEPEND": "dev-libs/B[bar(+)]" }, + #~ "dev-libs/A-6.4": { "EAPI": 4, "DEPEND": "dev-libs/B[bar(+)]" }, + + "dev-libs/B-1": {"EAPI": 1, "IUSE": "+foo"}, + } + + requests = ( + #~ (["=dev-libs/A-1.0"], {}, None, False, None), + (["=dev-libs/A-1.1"], {}, None, True, ["dev-libs/A-1.1"]), + (["=dev-libs/A-1.2"], {}, None, True, ["dev-libs/A-1.2"]), + (["=dev-libs/A-1.3"], {}, None, True, ["dev-libs/A-1.3"]), + #~ (["=dev-libs/A-1.4"], {}, None, True, ["dev-libs/A-1.4"]), + + #~ (["=dev-libs/A-2.0"], {}, None, False, None), + (["=dev-libs/A-2.1"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-2.1"]), + (["=dev-libs/A-2.2"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-2.2"]), + (["=dev-libs/A-2.3"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-2.3"]), + #~ (["=dev-libs/A-2.4"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-2.4"]), + + (["=dev-libs/A-3.0"], {}, None, False, None), + (["=dev-libs/A-3.1"], {}, None, False, None), + (["=dev-libs/A-3.2"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-3.2"]), + (["=dev-libs/A-3.3"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-3.3"]), + #~ (["=dev-libs/A-3.4"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-3.4"]), + + (["=dev-libs/A-4.0"], {}, None, False, None), + (["=dev-libs/A-4.1"], {}, None, False, None), + (["=dev-libs/A-4.2"], {}, None, True, ["dev-libs/A-4.2"]), + (["=dev-libs/A-4.3"], {}, None, True, ["dev-libs/A-4.3"]), + #~ (["=dev-libs/A-4.4"], {}, None, True, ["dev-libs/A-4.4"]), + + #~ (["=dev-libs/A-5.0"], {}, None, False, None), + #~ (["=dev-libs/A-5.1"], {}, None, False, None), + #~ (["=dev-libs/A-5.2"], {}, None, False, None), + #~ (["=dev-libs/A-5.3"], {}, None, False, None), + #~ (["=dev-libs/A-5.4"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-5.4"]), + + #~ (["=dev-libs/A-6.0"], {}, None, False, None), + #~ (["=dev-libs/A-6.1"], {}, None, False, None), + #~ (["=dev-libs/A-6.2"], {}, None, False, None), + #~ (["=dev-libs/A-6.3"], {}, None, False, None), + #~ (["=dev-libs/A-6.4"], {}, None, True, ["dev-libs/B-1", "dev-libs/A-6.4"]), + ) + + playground = ResolverPlayground(ebuilds=ebuilds) + try: + for atoms, options, action, \ + expected_result, expected_mergelist in requests: + result = playground.run(atoms, options, action) + self.assertEqual((result.success, result.mergelist), + (expected_result, expected_mergelist)) + finally: + playground.cleanup() diff --git a/pym/portage/tests/resolver/test_simple.py b/pym/portage/tests/resolver/test_simple.py new file mode 100644 index 00000000..ef19abe1 --- /dev/null +++ b/pym/portage/tests/resolver/test_simple.py @@ -0,0 +1,34 @@ +# Copyright 2010 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +from portage.tests import TestCase +from portage.tests.resolver.ResolverPlayground import ResolverPlayground + +class SimpleResolverTestCase(TestCase): + + def testSimple(self): + ebuilds = { + "dev-libs/A-1": {}, + "dev-libs/A-2": { "KEYWORDS": "~x86" }, + "dev-libs/B-1.2": {}, + } + installed = { + "dev-libs/B-1.1": {}, + } + + requests = ( + (["dev-libs/A"], {}, None, True, ["dev-libs/A-1"]), + (["=dev-libs/A-2"], {}, None, False, None), + (["dev-libs/B"], {"--noreplace": True}, None, True, []), + (["dev-libs/B"], {"--update": True}, None, True, ["dev-libs/B-1.2"]), + ) + + playground = ResolverPlayground(ebuilds=ebuilds, installed=installed) + try: + for atoms, options, action, \ + expected_result, expected_mergelist in requests: + result = playground.run(atoms, options, action) + self.assertEqual((result.success, result.mergelist), + (expected_result, expected_mergelist)) + finally: + playground.cleanup() diff --git a/pym/portage/tests/runTests b/pym/portage/tests/runTests index 8e8a3a44..a6f3b8f1 100755 --- a/pym/portage/tests/runTests +++ b/pym/portage/tests/runTests @@ -10,6 +10,12 @@ import os.path as osp # This line courtesy of Marienz and Pkgcore ;) sys.path.insert(0, osp.dirname(osp.dirname(osp.dirname(osp.abspath(__file__))))) +import portage + +# Ensure that we don't instantiate portage.settings, so that tests should +# work the same regardless of global configuration file state/existence. +portage._disable_legacy_globals() + import portage.tests as tests from portage.const import PORTAGE_BIN_PATH path = os.environ.get("PATH", "").split(":") @@ -19,6 +25,7 @@ if not path or not os.path.samefile(path[0], PORTAGE_BIN_PATH): os.environ["PATH"] = ":".join(path) del path + if __name__ == "__main__": result = tests.main() if not result.wasSuccessful(): diff --git a/pym/portage/util/_pty.py b/pym/portage/util/_pty.py index 7fba0e2b..877430e9 100644 --- a/pym/portage/util/_pty.py +++ b/pym/portage/util/_pty.py @@ -95,10 +95,7 @@ def _test_pty_eof(): buf = array.array('B') try: buf.fromfile(master_file, 1024) - except EOFError: - eof = True - except IOError: - # This is where data loss occurs. + except (EOFError, IOError): eof = True if not buf: diff --git a/pym/repoman/checks.py b/pym/repoman/checks.py index d403044b..7e76bf78 100644 --- a/pym/repoman/checks.py +++ b/pym/repoman/checks.py @@ -306,6 +306,57 @@ class EbuildQuotedA(LineCheck): if match: return "Quoted \"${A}\" on line: %d" +class EprefixifyDefined(LineCheck): + """ Check that prefix.eclass is inherited if needed""" + + repoman_check_name = 'eprefixify.defined' + + _eprefixify_re = re.compile(r'\beprefixify\b') + _inherit_prefix_re = re.compile(r'^\s*inherit\s(.*\s)?prefix\b') + + def new(self, pkg): + self._prefix_inherited = False + + def check(self, num, line): + if self._eprefixify_re.search(line) is not None: + if not self._prefix_inherited: + return errors.EPREFIXIFY_MISSING_INHERIT + elif self._inherit_prefix_re.search(line) is not None: + self._prefix_inherited = True + +class ImplicitRuntimeDeps(LineCheck): + """ + Detect the case where DEPEND is set and RDEPEND is unset in the ebuild, + since this triggers implicit RDEPEND=$DEPEND assignment (prior to EAPI 4). + """ + + repoman_check_name = 'RDEPEND.implicit' + _assignment_re = re.compile(r'^\s*(R?DEPEND)=') + + def new(self, pkg): + self._rdepend = False + self._depend = False + + def check_eapi(self, eapi): + # Beginning with EAPI 4, there is no + # implicit RDEPEND=$DEPEND assignment + # to be concerned with. + return eapi in ('0', '1', '2', '3') + + def check(self, num, line): + if not self._rdepend: + m = self._assignment_re.match(line) + if m is None: + pass + elif m.group(1) == "RDEPEND": + self._rdepend = True + elif m.group(1) == "DEPEND": + self._depend = True + + def end(self): + if self._depend and not self._rdepend: + yield 'RDEPEND is not explicitly assigned' + class InheritAutotools(LineCheck): """ Make sure appropriate functions are called in @@ -493,8 +544,8 @@ _constant_checks = tuple((c() for c in ( EbuildHeader, EbuildWhitespace, EbuildBlankLine, EbuildQuote, EbuildAssignment, Eapi3EbuildAssignment, EbuildUselessDodoc, EbuildUselessCdS, EbuildNestedDie, - EbuildPatches, EbuildQuotedA, EapiDefinition, - IUseUndefined, InheritAutotools, + EbuildPatches, EbuildQuotedA, EapiDefinition, EprefixifyDefined, + ImplicitRuntimeDeps, InheritAutotools, IUseUndefined, EMakeParallelDisabled, EMakeParallelDisabledViaMAKEOPTS, NoAsNeeded, DeprecatedBindnowFlags, SrcUnpackPatches, WantAutoDefaultValue, SrcCompileEconf, Eapi3DeprecatedFuncs, diff --git a/pym/repoman/errors.py b/pym/repoman/errors.py index 97bd2829..8a28d4fd 100644 --- a/pym/repoman/errors.py +++ b/pym/repoman/errors.py @@ -19,3 +19,4 @@ EAPI_DEFINED_AFTER_INHERIT = 'EAPI defined after inherit on line: %d' NO_AS_NEEDED = 'Upstream asneeded linking bug (no-as-needed on line: %d)' PRESERVE_OLD_LIB = 'Upstream ABI change workaround on line: %d' BUILT_WITH_USE = 'built_with_use on line: %d' +EPREFIXIFY_MISSING_INHERIT = "prefix.eclass is not inherited, but eprefixify is used on line: %d" |