Scheduler: terminate _merge_wait_queue
[portage.git] / pym / _emerge / Scheduler.py
index a97adc04131a9f8ec863b8843d78a2ce1a8f6866..b961e83c0e98d7e1321c483e0042c046f99a31e5 100644 (file)
@@ -1,33 +1,42 @@
-# Copyright 1999-2009 Gentoo Foundation
+# Copyright 1999-2011 Gentoo Foundation
 # Distributed under the terms of the GNU General Public License v2
-# $Id$
 
 from __future__ import print_function
 
-import codecs
+import gc
+import gzip
 import logging
+import shutil
+import signal
 import sys
+import tempfile
 import textwrap
 import time
+import warnings
 import weakref
+import zlib
 
 import portage
 from portage import StringIO
 from portage import os
 from portage import _encodings
-from portage import _unicode_decode
-from portage import _unicode_encode
+from portage import _unicode_decode, _unicode_encode
 from portage.cache.mappings import slot_dict_class
+from portage.const import LIBC_PACKAGE_ATOM
 from portage.elog.messages import eerror
-from portage.output import colorize, create_color_func, darkgreen, red
+from portage.localization import _
+from portage.output import colorize, create_color_func, red
 bad = create_color_func("BAD")
-from portage.sets import SETPREFIX
-from portage.sets.base import InternalPackageSet
+from portage._sets import SETPREFIX
+from portage._sets.base import InternalPackageSet
 from portage.util import writemsg, writemsg_level
 from portage.package.ebuild.digestcheck import digestcheck
 from portage.package.ebuild.digestgen import digestgen
+from portage.package.ebuild.prepare_build_dirs import prepare_build_dirs
 
+from _emerge.BinpkgFetcher import BinpkgFetcher
 from _emerge.BinpkgPrefetcher import BinpkgPrefetcher
+from _emerge.BinpkgVerifier import BinpkgVerifier
 from _emerge.Blocker import Blocker
 from _emerge.BlockerDB import BlockerDB
 from _emerge.clear_caches import clear_caches
@@ -38,6 +47,7 @@ from _emerge.depgraph import depgraph, resume_depgraph
 from _emerge.EbuildFetcher import EbuildFetcher
 from _emerge.EbuildPhase import EbuildPhase
 from _emerge.emergelog import emergelog, _emerge_log_dir
+from _emerge.FakeVartree import FakeVartree
 from _emerge._find_deep_system_runtime_deps import _find_deep_system_runtime_deps
 from _emerge._flush_elog_mod_echo import _flush_elog_mod_echo
 from _emerge.JobStatusDisplay import JobStatusDisplay
@@ -55,6 +65,9 @@ if sys.hexversion >= 0x3000000:
 
 class Scheduler(PollScheduler):
 
+       # max time between display status updates (milliseconds)
+       _max_display_latency = 3000
+
        _opts_ignore_blockers = \
                frozenset(["--buildpkgonly",
                "--fetchonly", "--fetch-all-uri",
@@ -74,7 +87,8 @@ class Scheduler(PollScheduler):
 
        class _iface_class(SlotObject):
                __slots__ = ("dblinkEbuildPhase", "dblinkDisplayMerge",
-                       "dblinkElog", "dblinkEmergeLog", "fetch", "register", "schedule",
+                       "dblinkElog", "dblinkEmergeLog", "fetch",
+                       "output", "register", "schedule",
                        "scheduleSetup", "scheduleUnpack", "scheduleYield",
                        "unregister")
 
@@ -130,17 +144,23 @@ class Scheduler(PollScheduler):
                        portage.exception.PortageException.__init__(self, value)
 
        def __init__(self, settings, trees, mtimedb, myopts,
-               spinner, mergelist, favorites, digraph):
+               spinner, mergelist=None, favorites=None, graph_config=None):
                PollScheduler.__init__(self)
+
+               if mergelist is not None:
+                       warnings.warn("The mergelist parameter of the " + \
+                               "_emerge.Scheduler constructor is now unused. Use " + \
+                               "the graph_config parameter instead.",
+                               DeprecationWarning, stacklevel=2)
+
                self.settings = settings
                self.target_root = settings["ROOT"]
                self.trees = trees
                self.myopts = myopts
                self._spinner = spinner
                self._mtimedb = mtimedb
-               self._mergelist = mergelist
                self._favorites = favorites
-               self._args_set = InternalPackageSet(favorites)
+               self._args_set = InternalPackageSet(favorites, allow_repo=True)
                self._build_opts = self._build_opts_class()
                for k in self._build_opts.__slots__:
                        setattr(self._build_opts, k, "--" + k.replace("_", "-") in myopts)
@@ -193,10 +213,8 @@ class Scheduler(PollScheduler):
                        self.edebug = 1
                self.pkgsettings = {}
                self._config_pool = {}
-               self._blocker_db = {}
-               for root in trees:
+               for root in self.trees:
                        self._config_pool[root] = []
-                       self._blocker_db[root] = BlockerDB(trees[root]["root_config"])
 
                fetch_iface = self._fetch_iface_class(log_file=self._fetch_log,
                        schedule=self._schedule_fetch)
@@ -205,7 +223,8 @@ class Scheduler(PollScheduler):
                        dblinkDisplayMerge=self._dblink_display_merge,
                        dblinkElog=self._dblink_elog,
                        dblinkEmergeLog=self._dblink_emerge_log,
-                       fetch=fetch_iface, register=self._register,
+                       fetch=fetch_iface, output=self._task_output,
+                       register=self._register,
                        schedule=self._schedule_wait,
                        scheduleSetup=self._schedule_setup,
                        scheduleUnpack=self._schedule_unpack,
@@ -214,6 +233,7 @@ class Scheduler(PollScheduler):
 
                self._prefetchers = weakref.WeakValueDictionary()
                self._pkg_queue = []
+               self._running_tasks = set()
                self._completed_tasks = set()
 
                self._failed_pkgs = []
@@ -221,7 +241,8 @@ class Scheduler(PollScheduler):
                self._failed_pkgs_die_msgs = []
                self._post_mod_echo_msgs = []
                self._parallel_fetch = False
-               merge_count = len([x for x in mergelist \
+               self._init_graph(graph_config)
+               merge_count = len([x for x in self._mergelist \
                        if isinstance(x, Package) and x.operation == "merge"])
                self._pkg_count = self._pkg_count_class(
                        curval=0, maxval=merge_count)
@@ -235,8 +256,6 @@ class Scheduler(PollScheduler):
                self._job_delay_exp = 1.5
                self._previous_job_start_time = None
 
-               self._set_digraph(digraph)
-
                # This is used to memoize the _choose_pkg() result when
                # no packages can be chosen until one of the existing
                # jobs completes.
@@ -256,7 +275,7 @@ class Scheduler(PollScheduler):
                                        "thus parallel-fetching is being disabled"+"\n",
                                        noiselevel=-1)
                                portage.writemsg(red("!!!")+"\n", noiselevel=-1)
-                       elif len(mergelist) > 1:
+                       elif merge_count > 1:
                                self._parallel_fetch = True
 
                if self._parallel_fetch:
@@ -274,9 +293,100 @@ class Scheduler(PollScheduler):
                        self._running_portage = self._pkg(cpv, "installed",
                                self._running_root, installed=True)
 
+       def _terminate_tasks(self):
+               self._status_display.quiet = True
+               # Remove running_tasks that have been added to queues but
+               # haven't been started yet, since we're going to discard
+               # them and their start/exit handlers won't be called.
+               for build in self._task_queues.jobs._task_queue:
+                       self._running_tasks.remove(build.pkg)
+               if self._merge_wait_queue:
+                       for merge in self._merge_wait_queue:
+                               self._running_tasks.remove(merge.merge.pkg)
+                       del self._merge_wait_queue[:]
+               for merge in self._task_queues.merge._task_queue:
+                       # Setup phases may be scheduled in this queue, but
+                       # we're only interested in the PackageMerge instances.
+                       if isinstance(merge, PackageMerge):
+                               self._running_tasks.remove(merge.merge.pkg)
+               for q in self._task_queues.values():
+                       q.clear()
+
+       def _init_graph(self, graph_config):
+               """
+               Initialization structures used for dependency calculations
+               involving currently installed packages.
+               """
+               # TODO: Replace the BlockerDB with a depgraph of installed packages
+               # that's updated incrementally with each upgrade/uninstall operation
+               # This will be useful for making quick and safe decisions with respect
+               # to aggressive parallelization discussed in bug #279623.
+               self._set_graph_config(graph_config)
+               self._blocker_db = {}
+               for root in self.trees:
+                       if graph_config is None:
+                               fake_vartree = FakeVartree(self.trees[root]["root_config"],
+                                       pkg_cache=self._pkg_cache)
+                       else:
+                               fake_vartree = graph_config.trees[root]['vartree']
+                       self._blocker_db[root] = BlockerDB(fake_vartree)
+
+       def _destroy_graph(self):
+               """
+               Use this to free memory at the beginning of _calc_resume_list().
+               After _calc_resume_list(), the _init_graph() method
+               must to be called in order to re-generate the structures that
+               this method destroys. 
+               """
+               self._blocker_db = None
+               self._set_graph_config(None)
+               gc.collect()
+
        def _poll(self, timeout=None):
+
                self._schedule()
-               PollScheduler._poll(self, timeout=timeout)
+
+               if timeout is None:
+                       while True:
+                               if not self._poll_event_handlers:
+                                       self._schedule()
+                                       if not self._poll_event_handlers:
+                                               raise StopIteration(
+                                                       "timeout is None and there are no poll() event handlers")
+                               previous_count = len(self._poll_event_queue)
+                               PollScheduler._poll(self, timeout=self._max_display_latency)
+                               self._status_display.display()
+                               if previous_count != len(self._poll_event_queue):
+                                       break
+
+               elif timeout <= self._max_display_latency:
+                       PollScheduler._poll(self, timeout=timeout)
+                       if timeout == 0:
+                               # The display is updated by _schedule() above, so it would be
+                               # redundant to update it here when timeout is 0.
+                               pass
+                       else:
+                               self._status_display.display()
+
+               else:
+                       remaining_timeout = timeout
+                       start_time = time.time()
+                       while True:
+                               previous_count = len(self._poll_event_queue)
+                               PollScheduler._poll(self,
+                                       timeout=min(self._max_display_latency, remaining_timeout))
+                               self._status_display.display()
+                               if previous_count != len(self._poll_event_queue):
+                                       break
+                               elapsed_time = time.time() - start_time
+                               if elapsed_time < 0:
+                                       # The system clock has changed such that start_time
+                                       # is now in the future, so just assume that the
+                                       # timeout has already elapsed.
+                                       break
+                               remaining_timeout = timeout - 1000 * elapsed_time
+                               if remaining_timeout <= 0:
+                                       break
 
        def _set_max_jobs(self, max_jobs):
                self._max_jobs = max_jobs
@@ -340,17 +450,40 @@ class Scheduler(PollScheduler):
                                interactive_tasks.append(task)
                return interactive_tasks
 
-       def _set_digraph(self, digraph):
+       def _set_graph_config(self, graph_config):
+
+               if graph_config is None:
+                       self._graph_config = None
+                       self._pkg_cache = {}
+                       self._digraph = None
+                       self._mergelist = []
+                       self._deep_system_deps.clear()
+                       return
+
+               self._graph_config = graph_config
+               self._pkg_cache = graph_config.pkg_cache
+               self._digraph = graph_config.graph
+               self._mergelist = graph_config.mergelist
+
                if "--nodeps" in self.myopts or \
                        (self._max_jobs is not True and self._max_jobs < 2):
                        # save some memory
                        self._digraph = None
+                       graph_config.graph = None
+                       graph_config.pkg_cache.clear()
+                       self._deep_system_deps.clear()
+                       for pkg in self._mergelist:
+                               self._pkg_cache[pkg] = pkg
                        return
 
-               self._digraph = digraph
                self._find_system_deps()
                self._prune_digraph()
                self._prevent_builddir_collisions()
+               self._implicit_libc_deps()
+               if '--debug' in self.myopts:
+                       writemsg("\nscheduler digraph:\n\n", noiselevel=-1)
+                       self._digraph.debug_print()
+                       writemsg("\n", noiselevel=-1)
 
        def _find_system_deps(self):
                """
@@ -412,6 +545,69 @@ class Scheduler(PollScheduler):
                                        priority=DepPriority(buildtime=True))
                        cpv_map[pkg.cpv].append(pkg)
 
+       def _implicit_libc_deps(self):
+               """
+               Create implicit dependencies on libc, in order to ensure that libc
+               is installed as early as possible (see bug #303567). If the merge
+               list contains both a new-style virtual and an old-style PROVIDE
+               virtual, the new-style virtual is used.
+               """
+               implicit_libc_roots = set([self._running_root.root])
+               libc_set = InternalPackageSet([LIBC_PACKAGE_ATOM])
+               norm_libc_pkgs = {}
+               virt_libc_pkgs = {}
+               for pkg in self._mergelist:
+                       if not isinstance(pkg, Package):
+                               # a satisfied blocker
+                               continue
+                       if pkg.installed:
+                               continue
+                       if pkg.root in implicit_libc_roots and \
+                               pkg.operation == 'merge':
+                               if libc_set.findAtomForPackage(pkg):
+                                       if pkg.category == 'virtual':
+                                               d = virt_libc_pkgs
+                                       else:
+                                               d = norm_libc_pkgs
+                                       if pkg.root in d:
+                                               raise AssertionError(
+                                                       "found 2 libc matches: %s and %s" % \
+                                                       (d[pkg.root], pkg))
+                                       d[pkg.root] = pkg
+
+               # Prefer new-style virtuals over old-style PROVIDE virtuals.
+               libc_pkg_map = norm_libc_pkgs.copy()
+               libc_pkg_map.update(virt_libc_pkgs)
+
+               # Only add a dep when the version changes.
+               for libc_pkg in list(libc_pkg_map.values()):
+                       if libc_pkg.root_config.trees['vartree'].dbapi.cpv_exists(
+                               libc_pkg.cpv):
+                               del libc_pkg_map[pkg.root]
+
+               if not libc_pkg_map:
+                       return
+
+               libc_pkgs = set(libc_pkg_map.values())
+               earlier_libc_pkgs = set()
+
+               for pkg in self._mergelist:
+                       if not isinstance(pkg, Package):
+                               # a satisfied blocker
+                               continue
+                       if pkg.installed:
+                               continue
+                       if pkg.root in implicit_libc_roots and \
+                               pkg.operation == 'merge':
+                               if pkg in libc_pkgs:
+                                       earlier_libc_pkgs.add(pkg)
+                               else:
+                                       my_libc = libc_pkg_map.get(pkg.root)
+                                       if my_libc is not None and \
+                                               my_libc in earlier_libc_pkgs:
+                                               self._digraph.add(my_libc, pkg,
+                                                       priority=DepPriority(buildtime=True))
+
        class _pkg_failure(portage.exception.PortageException):
                """
                An instance of this class is raised by unmerge() when
@@ -461,7 +657,6 @@ class Scheduler(PollScheduler):
                # Call gc.collect() here to avoid heap overflow that
                # triggers 'Cannot allocate memory' errors (reported
                # with python-2.5).
-               import gc
                gc.collect()
 
                blocker_db = self._blocker_db[new_pkg.root]
@@ -487,24 +682,13 @@ class Scheduler(PollScheduler):
                type_name = RootConfig.tree_pkg_map[pkg_dblink.treetype]
                root_config = self.trees[pkg_dblink.myroot]["root_config"]
                installed = type_name == "installed"
-               return self._pkg(cpv, type_name, root_config, installed=installed)
-
-       def _append_to_log_path(self, log_path, msg):
-
-               f = codecs.open(_unicode_encode(log_path,
-                       encoding=_encodings['fs'], errors='strict'),
-                       mode='a', encoding=_encodings['content'],
-                       errors='backslashreplace')
-               try:
-                       f.write(_unicode_decode(msg,
-                               encoding=_encodings['content'], errors='replace'))
-               finally:
-                       f.close()
+               repo = pkg_dblink.settings.get("PORTAGE_REPO_NAME")
+               return self._pkg(cpv, type_name, root_config,
+                       installed=installed, myrepo=repo)
 
        def _dblink_elog(self, pkg_dblink, phase, func, msgs):
 
                log_path = pkg_dblink.settings.get("PORTAGE_LOG_FILE")
-               background = self._background
                out = StringIO()
 
                for msg in msgs:
@@ -512,11 +696,7 @@ class Scheduler(PollScheduler):
 
                out_str = out.getvalue()
 
-               if not background:
-                       portage.util.writemsg_stdout(out_str, noiselevel=-1)
-
-               if log_path is not None:
-                       self._append_to_log_path(log_path, out_str)
+               self._task_output(out_str, log_path=log_path)
 
        def _dblink_emerge_log(self, msg):
                self._logger.log(msg)
@@ -530,10 +710,7 @@ class Scheduler(PollScheduler):
                                portage.util.writemsg_level(msg,
                                        level=level, noiselevel=noiselevel)
                else:
-                       if not background:
-                               portage.util.writemsg_level(msg,
-                                       level=level, noiselevel=noiselevel)
-                       self._append_to_log_path(log_path, msg)
+                       self._task_output(msg, log_path=log_path)
 
        def _dblink_ebuild_phase(self,
                pkg_dblink, pkg_dbapi, ebuild_path, phase):
@@ -545,18 +722,17 @@ class Scheduler(PollScheduler):
 
                scheduler = self._sched_iface
                settings = pkg_dblink.settings
-               pkg = self._dblink_pkg(pkg_dblink)
                background = self._background
                log_path = settings.get("PORTAGE_LOG_FILE")
 
                if phase in ('die_hooks', 'success_hooks'):
                        ebuild_phase = MiscFunctionsProcess(background=background,
-                               commands=[phase], phase=phase, pkg=pkg,
+                               commands=[phase], phase=phase,
                                scheduler=scheduler, settings=settings)
                else:
                        ebuild_phase = EbuildPhase(background=background,
-                               pkg=pkg, phase=phase, scheduler=scheduler,
-                               settings=settings, tree=pkg_dblink.treetype)
+                               phase=phase, scheduler=scheduler,
+                               settings=settings)
                ebuild_phase.start()
                ebuild_phase.wait()
 
@@ -575,6 +751,10 @@ class Scheduler(PollScheduler):
                digest = '--digest' in self.myopts
                if not digest:
                        for pkgsettings in self.pkgsettings.values():
+                               if pkgsettings.mycpv is not None:
+                                       # ensure that we are using global features
+                                       # settings rather than those from package.env
+                                       pkgsettings.reset()
                                if 'digest' in pkgsettings.features:
                                        digest = True
                                        break
@@ -588,11 +768,15 @@ class Scheduler(PollScheduler):
                                x.operation != 'merge':
                                continue
                        pkgsettings = self.pkgsettings[x.root]
+                       if pkgsettings.mycpv is not None:
+                               # ensure that we are using global features
+                               # settings rather than those from package.env
+                               pkgsettings.reset()
                        if '--digest' not in self.myopts and \
                                'digest' not in pkgsettings.features:
                                continue
                        portdb = x.root_config.trees['porttree'].dbapi
-                       ebuild_path = portdb.findname(x.cpv)
+                       ebuild_path = portdb.findname(x.cpv, myrepo=x.repo)
                        if ebuild_path is None:
                                raise AssertionError("ebuild not found for '%s'" % x.cpv)
                        pkgsettings['O'] = os.path.dirname(ebuild_path)
@@ -604,6 +788,37 @@ class Scheduler(PollScheduler):
 
                return os.EX_OK
 
+       def _env_sanity_check(self):
+               """
+               Verify a sane environment before trying to build anything from source.
+               """
+               have_src_pkg = False
+               for x in self._mergelist:
+                       if isinstance(x, Package) and not x.built:
+                               have_src_pkg = True
+                               break
+
+               if not have_src_pkg:
+                       return os.EX_OK
+
+               for settings in self.pkgsettings.values():
+                       for var in ("ARCH", ):
+                               value = settings.get(var)
+                               if value and value.strip():
+                                       continue
+                               msg = _("%(var)s is not set... "
+                                       "Are you missing the '%(configroot)setc/make.profile' symlink? "
+                                       "Is the symlink correct? "
+                                       "Is your portage tree complete?") % \
+                                       {"var": var, "configroot": settings["PORTAGE_CONFIGROOT"]}
+
+                               out = portage.output.EOutput()
+                               for line in textwrap.wrap(msg, 70):
+                                       out.eerror(line)
+                               return 1
+
+               return os.EX_OK
+
        def _check_manifests(self):
                # Verify all the manifests now so that the user is notified of failure
                # as soon as possible.
@@ -638,7 +853,7 @@ class Scheduler(PollScheduler):
                        root_config = x.root_config
                        portdb = root_config.trees["porttree"].dbapi
                        quiet_config = quiet_settings[root_config.root]
-                       ebuild_path = portdb.findname(x.cpv)
+                       ebuild_path = portdb.findname(x.cpv, myrepo=x.repo)
                        if ebuild_path is None:
                                raise AssertionError("ebuild not found for '%s'" % x.cpv)
                        quiet_config["O"] = os.path.dirname(ebuild_path)
@@ -728,9 +943,13 @@ class Scheduler(PollScheduler):
                if pkg.root == self._running_root.root and \
                        portage.match_from_list(
                        portage.const.PORTAGE_PACKAGE_ATOM, [pkg]):
-                       if self._running_portage:
-                               return pkg.cpv != self._running_portage.cpv
-                       return True
+                       if self._running_portage is None:
+                               return True
+                       elif pkg.cpv != self._running_portage.cpv or \
+                               '9999' in pkg.cpv or \
+                               'git' in pkg.inherited or \
+                               'git-2' in pkg.inherited:
+                               return True
                return False
 
        def _restart_if_necessary(self, pkg):
@@ -778,14 +997,127 @@ class Scheduler(PollScheduler):
                        if myopt not in bad_resume_opts:
                                if myarg is True:
                                        mynewargv.append(myopt)
+                               elif isinstance(myarg, list):
+                                       # arguments like --exclude that use 'append' action
+                                       for x in myarg:
+                                               mynewargv.append("%s=%s" % (myopt, x))
                                else:
-                                       mynewargv.append(myopt +"="+ str(myarg))
+                                       mynewargv.append("%s=%s" % (myopt, myarg))
                # priority only needs to be adjusted on the first run
                os.environ["PORTAGE_NICENESS"] = "0"
                os.execv(mynewargv[0], mynewargv)
 
-       def merge(self):
+       def _run_pkg_pretend(self):
+               """
+               Since pkg_pretend output may be important, this method sends all
+               output directly to stdout (regardless of options like --quiet or
+               --jobs).
+               """
+
+               failures = 0
+
+               # Use a local PollScheduler instance here, since we don't
+               # want tasks here to trigger the usual Scheduler callbacks
+               # that handle job scheduling and status display.
+               sched_iface = PollScheduler().sched_iface
+
+               for x in self._mergelist:
+                       if not isinstance(x, Package):
+                               continue
+
+                       if x.operation == "uninstall":
+                               continue
+
+                       if x.metadata["EAPI"] in ("0", "1", "2", "3"):
+                               continue
+
+                       if "pretend" not in x.metadata.defined_phases:
+                               continue
+
+                       out_str =">>> Running pre-merge checks for " + colorize("INFORM", x.cpv) + "\n"
+                       portage.util.writemsg_stdout(out_str, noiselevel=-1)
+
+                       root_config = x.root_config
+                       settings = self.pkgsettings[root_config.root]
+                       settings.setcpv(x)
+                       tmpdir = tempfile.mkdtemp()
+                       tmpdir_orig = settings["PORTAGE_TMPDIR"]
+                       settings["PORTAGE_TMPDIR"] = tmpdir
+
+                       try:
+                               if x.built:
+                                       tree = "bintree"
+                                       bintree = root_config.trees["bintree"].dbapi.bintree
+                                       fetched = False
+
+                                       # Display fetch on stdout, so that it's always clear what
+                                       # is consuming time here.
+                                       if bintree.isremote(x.cpv):
+                                               fetcher = BinpkgFetcher(pkg=x,
+                                                       scheduler=sched_iface)
+                                               fetcher.start()
+                                               if fetcher.wait() != os.EX_OK:
+                                                       failures += 1
+                                                       continue
+                                               fetched = fetcher.pkg_path
+
+                                       verifier = BinpkgVerifier(pkg=x,
+                                               scheduler=sched_iface)
+                                       verifier.start()
+                                       if verifier.wait() != os.EX_OK:
+                                               failures += 1
+                                               continue
+
+                                       if fetched:
+                                               bintree.inject(x.cpv, filename=fetched)
+                                       tbz2_file = bintree.getname(x.cpv)
+                                       infloc = os.path.join(tmpdir, x.category, x.pf, "build-info")
+                                       os.makedirs(infloc)
+                                       portage.xpak.tbz2(tbz2_file).unpackinfo(infloc)
+                                       ebuild_path = os.path.join(infloc, x.pf + ".ebuild")
+                                       settings.configdict["pkg"]["EMERGE_FROM"] = "binary"
+                                       settings.configdict["pkg"]["MERGE_TYPE"] = "binary"
+
+                               else:
+                                       tree = "porttree"
+                                       portdb = root_config.trees["porttree"].dbapi
+                                       ebuild_path = portdb.findname(x.cpv, myrepo=x.repo)
+                                       if ebuild_path is None:
+                                               raise AssertionError("ebuild not found for '%s'" % x.cpv)
+                                       settings.configdict["pkg"]["EMERGE_FROM"] = "ebuild"
+                                       if self._build_opts.buildpkgonly:
+                                               settings.configdict["pkg"]["MERGE_TYPE"] = "buildonly"
+                                       else:
+                                               settings.configdict["pkg"]["MERGE_TYPE"] = "source"
+
+                               portage.package.ebuild.doebuild.doebuild_environment(ebuild_path,
+                                       "pretend", settings=settings,
+                                       db=self.trees[settings["ROOT"]][tree].dbapi)
+                               prepare_build_dirs(root_config.root, settings, cleanup=0)
+
+                               vardb = root_config.trees['vartree'].dbapi
+                               settings["REPLACING_VERSIONS"] = " ".join(
+                                       set(portage.versions.cpv_getversion(match) \
+                                               for match in vardb.match(x.slot_atom) + \
+                                               vardb.match('='+x.cpv)))
+                               pretend_phase = EbuildPhase(
+                                       phase="pretend", scheduler=sched_iface,
+                                       settings=settings)
+
+                               pretend_phase.start()
+                               ret = pretend_phase.wait()
+                               if ret != os.EX_OK:
+                                       failures += 1
+                               portage.elog.elog_process(x.cpv, settings)
+                       finally:
+                               shutil.rmtree(tmpdir)
+                               settings["PORTAGE_TMPDIR"] = tmpdir_orig
 
+               if failures:
+                       return 1
+               return os.EX_OK
+
+       def merge(self):
                if "--resume" in self.myopts:
                        # We're resuming.
                        portage.writemsg_stdout(
@@ -835,14 +1167,51 @@ class Scheduler(PollScheduler):
                if rval != os.EX_OK:
                        return rval
 
+               rval = self._env_sanity_check()
+               if rval != os.EX_OK:
+                       return rval
+
                # TODO: Immediately recalculate deps here if --keep-going
                #       is enabled and corrupt manifests are detected.
                rval = self._check_manifests()
                if rval != os.EX_OK and not keep_going:
                        return rval
 
+               rval = self._run_pkg_pretend()
+               if rval != os.EX_OK:
+                       return rval
+
                while True:
-                       rval = self._merge()
+
+                       received_signal = []
+
+                       def sighandler(signum, frame):
+                               signal.signal(signal.SIGINT, signal.SIG_IGN)
+                               signal.signal(signal.SIGTERM, signal.SIG_IGN)
+                               portage.util.writemsg("\n\nExiting on signal %(signal)s\n" % \
+                                       {"signal":signum})
+                               self.terminate()
+                               received_signal.append(128 + signum)
+
+                       earlier_sigint_handler = signal.signal(signal.SIGINT, sighandler)
+                       earlier_sigterm_handler = signal.signal(signal.SIGTERM, sighandler)
+
+                       try:
+                               rval = self._merge()
+                       finally:
+                               # Restore previous handlers
+                               if earlier_sigint_handler is not None:
+                                       signal.signal(signal.SIGINT, earlier_sigint_handler)
+                               else:
+                                       signal.signal(signal.SIGINT, signal.SIG_DFL)
+                               if earlier_sigterm_handler is not None:
+                                       signal.signal(signal.SIGTERM, earlier_sigterm_handler)
+                               else:
+                                       signal.signal(signal.SIGTERM, signal.SIG_DFL)
+
+                       if received_signal:
+                               sys.exit(received_signal[0])
+
                        if rval == os.EX_OK or fetchonly or not keep_going:
                                break
                        if "resume" not in mtimedb:
@@ -897,16 +1266,22 @@ class Scheduler(PollScheduler):
                        log_path = self._locate_failure_log(failed_pkg)
                        if log_path is not None:
                                try:
-                                       log_file = codecs.open(_unicode_encode(log_path,
-                                       encoding=_encodings['fs'], errors='strict'),
-                                       mode='r', encoding=_encodings['content'], errors='replace')
+                                       log_file = open(_unicode_encode(log_path,
+                                               encoding=_encodings['fs'], errors='strict'), mode='rb')
                                except IOError:
                                        pass
+                               else:
+                                       if log_path.endswith('.gz'):
+                                               log_file =  gzip.GzipFile(filename='',
+                                                       mode='rb', fileobj=log_file)
 
                        if log_file is not None:
                                try:
                                        for line in log_file:
                                                writemsg_level(line, noiselevel=-1)
+                               except zlib.error as e:
+                                       writemsg_level("%s\n" % (e,), level=logging.ERROR,
+                                               noiselevel=-1)
                                finally:
                                        log_file.close()
                                failure_log_shown = True
@@ -943,7 +1318,7 @@ class Scheduler(PollScheduler):
                                msg()
 
                if len(self._failed_pkgs_all) > 1 or \
-                       (self._failed_pkgs_all and "--keep-going" in self.myopts):
+                       (self._failed_pkgs_all and keep_going):
                        if len(self._failed_pkgs_all) > 1:
                                msg = "The following %d packages have " % \
                                        len(self._failed_pkgs_all) + \
@@ -957,7 +1332,9 @@ class Scheduler(PollScheduler):
                                printer.eerror(line)
                        printer.eerror("")
                        for failed_pkg in self._failed_pkgs_all:
-                               msg = " %s" % (colorize('INFORM', failed_pkg.pkg.__str__()),)
+                               # Use _unicode_decode() to force unicode format string so
+                               # that Package.__unicode__() is called in python2.
+                               msg = _unicode_decode(" %s") % (failed_pkg.pkg,)
                                log_path = self._locate_failure_log(failed_pkg)
                                if log_path is not None:
                                        msg += ", Log file:"
@@ -1063,6 +1440,7 @@ class Scheduler(PollScheduler):
 
        def _do_merge_exit(self, merge):
                pkg = merge.merge.pkg
+               self._running_tasks.remove(pkg)
                if merge.returncode != os.EX_OK:
                        settings = merge.merge.settings
                        build_dir = settings.get("PORTAGE_BUILDDIR")
@@ -1072,9 +1450,9 @@ class Scheduler(PollScheduler):
                                build_dir=build_dir, build_log=build_log,
                                pkg=pkg,
                                returncode=merge.returncode))
-                       self._failed_pkg_msg(self._failed_pkgs[-1], "install", "to")
-
-                       self._status_display.failed = len(self._failed_pkgs)
+                       if not self._terminated_tasks:
+                               self._failed_pkg_msg(self._failed_pkgs[-1], "install", "to")
+                               self._status_display.failed = len(self._failed_pkgs)
                        return
 
                self._task_complete(pkg)
@@ -1082,9 +1460,15 @@ class Scheduler(PollScheduler):
                if pkg_to_replace is not None:
                        # When a package is replaced, mark it's uninstall
                        # task complete (if any).
-                       uninst_hash_key = \
-                               ("installed", pkg.root, pkg_to_replace.cpv, "uninstall")
-                       self._task_complete(uninst_hash_key)
+                       if self._digraph is not None and \
+                               pkg_to_replace in self._digraph:
+                               try:
+                                       self._pkg_queue.remove(pkg_to_replace)
+                               except ValueError:
+                                       pass
+                               self._task_complete(pkg_to_replace)
+                       else:
+                               self._pkg_cache.pop(pkg_to_replace, None)
 
                if pkg.installed:
                        return
@@ -1101,7 +1485,13 @@ class Scheduler(PollScheduler):
                mtimedb.commit()
 
        def _build_exit(self, build):
-               if build.returncode == os.EX_OK:
+               if build.returncode == os.EX_OK and self._terminated_tasks:
+                       # We've been interrupted, so we won't
+                       # add this to the merge queue.
+                       self.curval += 1
+                       self._running_tasks.remove(build.pkg)
+                       self._deallocate_config(build.settings)
+               elif build.returncode == os.EX_OK:
                        self.curval += 1
                        merge = PackageMerge(merge=build)
                        if not build.build_opts.buildpkgonly and \
@@ -1115,6 +1505,7 @@ class Scheduler(PollScheduler):
                                self._task_queues.merge.add(merge)
                                self._status_display.merges = len(self._task_queues.merge)
                else:
+                       self._running_tasks.remove(build.pkg)
                        settings = build.settings
                        build_dir = settings.get("PORTAGE_BUILDDIR")
                        build_log = settings.get("PORTAGE_LOG_FILE")
@@ -1123,9 +1514,9 @@ class Scheduler(PollScheduler):
                                build_dir=build_dir, build_log=build_log,
                                pkg=build.pkg,
                                returncode=build.returncode))
-                       self._failed_pkg_msg(self._failed_pkgs[-1], "emerge", "for")
-
-                       self._status_display.failed = len(self._failed_pkgs)
+                       if not self._terminated_tasks:
+                               self._failed_pkg_msg(self._failed_pkgs[-1], "emerge", "for")
+                               self._status_display.failed = len(self._failed_pkgs)
                        self._deallocate_config(build.settings)
                self._jobs -= 1
                self._status_display.running = self._jobs
@@ -1169,6 +1560,7 @@ class Scheduler(PollScheduler):
                self._status_display.reset()
                self._digraph = None
                self._task_queues.fetch.clear()
+               self._prefetchers.clear()
 
        def _choose_pkg(self):
                """
@@ -1179,14 +1571,14 @@ class Scheduler(PollScheduler):
                        return None
 
                if self._digraph is None:
-                       if (self._jobs or self._task_queues.merge) and \
+                       if self._is_work_scheduled() and \
                                not ("--nodeps" in self.myopts and \
                                (self._max_jobs is True or self._max_jobs > 1)):
                                self._choose_pkg_return_early = True
                                return None
                        return self._pkg_queue.pop(0)
 
-               if not (self._jobs or self._task_queues.merge):
+               if not self._is_work_scheduled():
                        return self._pkg_queue.pop(0)
 
                self._prune_digraph()
@@ -1255,7 +1647,11 @@ class Scheduler(PollScheduler):
                                node in later):
                                dependent = True
                                break
-                       node_stack.extend(graph.child_nodes(node))
+
+                       # Don't traverse children of uninstall nodes since
+                       # those aren't dependencies in the usual sense.
+                       if node.operation != "uninstall":
+                               node_stack.extend(graph.child_nodes(node))
 
                return dependent
 
@@ -1286,54 +1682,57 @@ class Scheduler(PollScheduler):
                        self._opts_no_background.intersection(self.myopts):
                        self._set_max_jobs(1)
 
-               merge_queue = self._task_queues.merge
-
                while self._schedule():
                        if self._poll_event_handlers:
                                self._poll_loop()
 
                while True:
                        self._schedule()
-                       if not (self._jobs or merge_queue):
+                       if not self._is_work_scheduled():
                                break
                        if self._poll_event_handlers:
                                self._poll_loop()
 
        def _keep_scheduling(self):
-               return bool(self._pkg_queue and \
+               return bool(not self._terminated_tasks and self._pkg_queue and \
                        not (self._failed_pkgs and not self._build_opts.fetchonly))
 
-       def _schedule_tasks(self):
-
-               # When the number of jobs drops to zero, process all waiting merges.
-               if not self._jobs and self._merge_wait_queue:
-                       for task in self._merge_wait_queue:
-                               task.addExitListener(self._merge_wait_exit_handler)
-                               self._task_queues.merge.add(task)
-                       self._status_display.merges = len(self._task_queues.merge)
-                       self._merge_wait_scheduled.extend(self._merge_wait_queue)
-                       del self._merge_wait_queue[:]
+       def _is_work_scheduled(self):
+               return bool(self._running_tasks)
 
-               self._schedule_tasks_imp()
-               self._status_display.display()
+       def _schedule_tasks(self):
 
-               state_change = 0
-               for q in self._task_queues.values():
-                       if q.schedule():
-                               state_change += 1
+               while True:
 
-               # Cancel prefetchers if they're the only reason
-               # the main poll loop is still running.
-               if self._failed_pkgs and not self._build_opts.fetchonly and \
-                       not (self._jobs or self._task_queues.merge) and \
-                       self._task_queues.fetch:
-                       self._task_queues.fetch.clear()
-                       state_change += 1
+                       # When the number of jobs drops to zero, process all waiting merges.
+                       if not self._jobs and self._merge_wait_queue:
+                               for task in self._merge_wait_queue:
+                                       task.addExitListener(self._merge_wait_exit_handler)
+                                       self._task_queues.merge.add(task)
+                               self._status_display.merges = len(self._task_queues.merge)
+                               self._merge_wait_scheduled.extend(self._merge_wait_queue)
+                               del self._merge_wait_queue[:]
 
-               if state_change:
                        self._schedule_tasks_imp()
                        self._status_display.display()
 
+                       state_change = 0
+                       for q in self._task_queues.values():
+                               if q.schedule():
+                                       state_change += 1
+
+                       # Cancel prefetchers if they're the only reason
+                       # the main poll loop is still running.
+                       if self._failed_pkgs and not self._build_opts.fetchonly and \
+                               not self._is_work_scheduled() and \
+                               self._task_queues.fetch:
+                               self._task_queues.fetch.clear()
+                               state_change += 1
+
+                       if not (state_change or \
+                               (not self._jobs and self._merge_wait_queue)):
+                               break
+
                return self._keep_scheduling()
 
        def _job_delay(self):
@@ -1384,6 +1783,7 @@ class Scheduler(PollScheduler):
                                self._pkg_count.curval += 1
 
                        task = self._task(pkg)
+                       self._running_tasks.add(pkg)
 
                        if pkg.installed:
                                merge = PackageMerge(merge=task)
@@ -1412,10 +1812,14 @@ class Scheduler(PollScheduler):
                if pkg.operation != "uninstall":
                        vardb = pkg.root_config.trees["vartree"].dbapi
                        previous_cpv = vardb.match(pkg.slot_atom)
+                       if not previous_cpv and vardb.cpv_exists(pkg.cpv):
+                               # same cpv, different SLOT
+                               previous_cpv = [pkg.cpv]
                        if previous_cpv:
                                previous_cpv = previous_cpv.pop()
                                pkg_to_replace = self._pkg(previous_cpv,
-                                       "installed", pkg.root_config, installed=True)
+                                       "installed", pkg.root_config, installed=True,
+                                       operation="uninstall")
 
                task = MergeListItem(args_set=self._args_set,
                        background=self._background, binpkg_opts=self._binpkg_opts,
@@ -1495,6 +1899,10 @@ class Scheduler(PollScheduler):
                """
                print(colorize("GOOD", "*** Resuming merge..."))
 
+               # free some memory before creating
+               # the resume depgraph
+               self._destroy_graph()
+
                myparams = create_depgraph_params(self.myopts, None)
                success = False
                e = None
@@ -1555,12 +1963,7 @@ class Scheduler(PollScheduler):
                        self._post_mod_echo_msgs.append(mydepgraph.display_problems)
                        return False
                mydepgraph.display_problems()
-
-               mylist = mydepgraph.altlist()
-               mydepgraph.break_refs(mylist)
-               mydepgraph.break_refs(dropped_tasks)
-               self._mergelist = mylist
-               self._set_digraph(mydepgraph.schedulerGraph())
+               self._init_graph(mydepgraph.schedulerGraph())
 
                msg_width = 75
                for task in dropped_tasks:
@@ -1647,29 +2050,46 @@ class Scheduler(PollScheduler):
                        if world_locked:
                                world_set.unlock()
 
-       def _pkg(self, cpv, type_name, root_config, installed=False):
+       def _pkg(self, cpv, type_name, root_config, installed=False,
+               operation=None, myrepo=None):
                """
                Get a package instance from the cache, or create a new
                one if necessary. Raises KeyError from aux_get if it
                failures for some reason (package does not exist or is
                corrupt).
                """
-               operation = "merge"
-               if installed:
-                       operation = "nomerge"
 
-               if self._digraph is not None:
-                       # Reuse existing instance when available.
-                       pkg = self._digraph.get(
-                               (type_name, root_config.root, cpv, operation))
-                       if pkg is not None:
-                               return pkg
+               if type_name != "ebuild":
+                       # For installed (and binary) packages we don't care for the repo
+                       # when it comes to hashing, because there can only be one cpv.
+                       # So overwrite the repo_key with type_name.
+                       repo_key = type_name
+                       myrepo = None
+               elif myrepo is None:
+                       raise AssertionError(
+                               "Scheduler._pkg() called without 'myrepo' argument")
+               else:
+                       repo_key = myrepo
+
+               if operation is None:
+                       if installed:
+                               operation = "nomerge"
+                       else:
+                               operation = "merge"
+
+               # Reuse existing instance when available.
+               pkg = self._pkg_cache.get(
+                       (type_name, root_config.root, cpv, operation, repo_key))
+               if pkg is not None:
+                       return pkg
 
                tree_type = depgraph.pkg_tree_map[type_name]
                db = root_config.trees[tree_type].dbapi
                db_keys = list(self.trees[root_config.root][
                        tree_type].dbapi._aux_cache_keys)
-               metadata = zip(db_keys, db.aux_get(cpv, db_keys))
-               return Package(built=(type_name != 'ebuild'),
-                       cpv=cpv, metadata=metadata,
-                       root_config=root_config, installed=installed)
+               metadata = zip(db_keys, db.aux_get(cpv, db_keys, myrepo=myrepo))
+               pkg = Package(built=(type_name != "ebuild"),
+                       cpv=cpv, installed=installed, metadata=metadata,
+                       root_config=root_config, type_name=type_name)
+               self._pkg_cache[pkg] = pkg
+               return pkg