Old "Change USE" message: Don't screw up if the highest version is masked and parent...
[portage.git] / bin / emaint
index 174b4540dd3e28df3ed61d7a055ee2747ef0c004..5ff60422c7e5ad188237dd70383cce26f90792f7 100755 (executable)
@@ -1,10 +1,15 @@
 #!/usr/bin/python -O
+# vim: noet :
+
+from __future__ import print_function
 
-import sys, os, time, signal
-from optparse import OptionParser, OptionValueError
-if not hasattr(__builtins__, "set"):
-       from sets import Set as set
 import re
+import signal
+import sys
+import textwrap
+import time
+from optparse import OptionParser, OptionValueError
+
 try:
        import portage
 except ImportError:
@@ -12,9 +17,12 @@ except ImportError:
        sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
        import portage
 
-import portage.const, portage.exception, portage.output
+from portage import os
+
 class WorldHandler(object):
 
+       short_desc = "Fix problems in the world file"
+
        def name():
                return "world"
        name = staticmethod(name)
@@ -24,6 +32,10 @@ class WorldHandler(object):
                self.not_installed = []
                self.invalid_category = []
                self.okay = []
+               from portage._sets import load_default_config
+               setconfig = load_default_config(portage.settings,
+                       portage.db[portage.settings["ROOT"]])
+               self._sets = setconfig.getSets()
 
        def _check_world(self, onProgress):
                categories = set(portage.settings.categories)
@@ -32,13 +44,22 @@ class WorldHandler(object):
                self.found = os.access(self.world_file, os.R_OK)
                vardb = portage.db[myroot]["vartree"].dbapi
 
-               world_atoms = open(self.world_file).read().split()
+               from portage._sets import SETPREFIX
+               sets = self._sets
+               world_atoms = list(sets["selected"])
                maxval = len(world_atoms)
                if onProgress:
                        onProgress(maxval, 0)
                for i, atom in enumerate(world_atoms):
-                       if not portage.isvalidatom(atom):
-                               self.invalid.append(atom)
+                       if not isinstance(atom, portage.dep.Atom):
+                               if atom.startswith(SETPREFIX):
+                                       s = atom[len(SETPREFIX):]
+                                       if s in sets:
+                                               self.okay.append(atom)
+                                       else:
+                                               self.not_installed.append(atom)
+                               else:
+                                       self.invalid.append(atom)
                                if onProgress:
                                        onProgress(maxval, i+1)
                                continue
@@ -46,7 +67,7 @@ class WorldHandler(object):
                        if not vardb.match(atom):
                                self.not_installed.append(atom)
                                okay = False
-                       if portage.catsplit(atom)[0] not in categories:
+                       if portage.catsplit(atom.cp)[0] not in categories:
                                self.invalid_category.append(atom)
                                okay = False
                        if okay:
@@ -58,24 +79,36 @@ class WorldHandler(object):
                self._check_world(onProgress)
                errors = []
                if self.found:
-                       errors += map(lambda x: "'%s' is not a valid atom" % x, self.invalid)
-                       errors += map(lambda x: "'%s' is not installed" % x, self.not_installed)
-                       errors += map(lambda x: "'%s' has a category that is not listed in /etc/portage/categories" % x, self.invalid_category)
+                       errors += ["'%s' is not a valid atom" % x for x in self.invalid]
+                       errors += ["'%s' is not installed" % x for x in self.not_installed]
+                       errors += ["'%s' has a category that is not listed in /etc/portage/categories" % x for x in self.invalid_category]
                else:
                        errors.append(self.world_file + " could not be opened for reading")
                return errors
 
        def fix(self, onProgress=None):
-               self._check_world(onProgress)
-               errors = []
+               world_set = self._sets["selected"]
+               world_set.lock()
                try:
-                       portage.write_atomic(self.world_file, "\n".join(self.okay))
-               except portage.exception.PortageException:
-                       errors.append(self.world_file + " could not be opened for writing")
-               return errors
+                       world_set.load() # maybe it's changed on disk
+                       before = set(world_set)
+                       self._check_world(onProgress)
+                       after = set(self.okay)
+                       errors = []
+                       if before != after:
+                               try:
+                                       world_set.replace(self.okay)
+                               except portage.exception.PortageException:
+                                       errors.append("%s could not be opened for writing" % \
+                                               self.world_file)
+                       return errors
+               finally:
+                       world_set.unlock()
 
 class BinhostHandler(object):
 
+       short_desc = "Generate a metadata index for binary packages"
+
        def name():
                return "binhost"
        name = staticmethod(name)
@@ -84,14 +117,8 @@ class BinhostHandler(object):
                myroot = portage.settings["ROOT"]
                self._bintree = portage.db[myroot]["bintree"]
                self._bintree.populate()
-               self._pkgindex_file = os.path.join(self._bintree.pkgdir, "Packages")
-               from portage import getbinpkg
-               self._pkgindex = getbinpkg.PackageIndex()
-               f = open(self._pkgindex_file, 'r')
-               try:
-                       self._pkgindex.read(f)
-               finally:
-                       f.close()
+               self._pkgindex_file = self._bintree._pkgindex_file
+               self._pkgindex = self._bintree._load_pkgindex()
 
        def check(self, onProgress=None):
                missing = []
@@ -102,14 +129,17 @@ class BinhostHandler(object):
                        onProgress(maxval, 0)
                pkgindex = self._pkgindex
                missing = []
+               metadata = {}
+               for d in pkgindex.packages:
+                       metadata[d["CPV"]] = d
                for i, cpv in enumerate(cpv_all):
-                       d = pkgindex.packages.get(cpv)
+                       d = metadata.get(cpv)
                        if not d or "MD5" not in d:
                                missing.append(cpv)
                        if onProgress:
                                onProgress(maxval, i+1)
                errors = ["'%s' is not in Packages" % cpv for cpv in missing]
-               stale = set(pkgindex.packages).difference(cpv_all)
+               stale = set(metadata).difference(cpv_all)
                for cpv in stale:
                        errors.append("'%s' is not in the repository" % cpv)
                return errors
@@ -119,40 +149,66 @@ class BinhostHandler(object):
                cpv_all = self._bintree.dbapi.cpv_all()
                cpv_all.sort()
                missing = []
-               maxval = len(cpv_all)
+               maxval = 0
                if onProgress:
                        onProgress(maxval, 0)
                pkgindex = self._pkgindex
                missing = []
+               metadata = {}
+               for d in pkgindex.packages:
+                       metadata[d["CPV"]] = d
+
                for i, cpv in enumerate(cpv_all):
-                       d = pkgindex.packages.get(cpv)
+                       d = metadata.get(cpv)
                        if not d or "MD5" not in d:
-                               bintree.inject(cpv)
-                       if onProgress:
-                               onProgress(maxval, i+1)
-               stale = set(pkgindex.packages).difference(cpv_all)
-               if stale:
+                               missing.append(cpv)
+
+               stale = set(metadata).difference(cpv_all)
+               if missing or stale:
                        from portage import locks
                        pkgindex_lock = locks.lockfile(
                                self._pkgindex_file, wantnewlockfile=1)
                        try:
-                               from portage import getbinpkg
-                               del pkgindex
-                               self._pkgindex = getbinpkg.PackageIndex()
-                               f = open(self._pkgindex_file, 'r')
-                               try:
-                                       self._pkgindex.read(f)
-                               finally:
-                                       f.close()
-                               from portage.dbapi.bintree import binarytree
-                               self._bintree = binarytree(bintree.root, bintree.pkgdir,
-                                       settings=bintree.settings)
-                               del bintree
-                               portage.db[self._bintree.root]["bintree"] = self._bintree
-                               self._bintree._populate()
-                               for cpv in set(self._pkgindex.packages).difference(
+                               # Repopulate with lock held.
+                               bintree._populate()
+                               cpv_all = self._bintree.dbapi.cpv_all()
+                               cpv_all.sort()
+
+                               pkgindex = bintree._load_pkgindex()
+                               self._pkgindex = pkgindex
+
+                               metadata = {}
+                               for d in pkgindex.packages:
+                                       metadata[d["CPV"]] = d
+
+                               # Recount missing packages, with lock held.
+                               del missing[:]
+                               for i, cpv in enumerate(cpv_all):
+                                       d = metadata.get(cpv)
+                                       if not d or "MD5" not in d:
+                                               missing.append(cpv)
+
+                               maxval = len(missing)
+                               for i, cpv in enumerate(missing):
+                                       try:
+                                               metadata[cpv] = bintree._pkgindex_entry(cpv)
+                                       except portage.exception.InvalidDependString:
+                                               writemsg("!!! Invalid binary package: '%s'\n" % \
+                                                       bintree.getname(cpv), noiselevel=-1)
+
+                                       if onProgress:
+                                               onProgress(maxval, i+1)
+
+                               for cpv in set(metadata).difference(
                                        self._bintree.dbapi.cpv_all()):
-                                       del self._pkgindex.packages[cpv]
+                                       del metadata[cpv]
+
+                               # We've updated the pkgindex, so set it to
+                               # repopulate when necessary.
+                               bintree.populated = False
+
+                               del pkgindex.packages[:]
+                               pkgindex.packages.extend(metadata.values())
                                from portage.util import atomic_ofstream
                                f = atomic_ofstream(self._pkgindex_file)
                                try:
@@ -161,52 +217,83 @@ class BinhostHandler(object):
                                        f.close()
                        finally:
                                locks.unlockfile(pkgindex_lock)
+
+               if onProgress:
+                       if maxval == 0:
+                               maxval = 1
+                       onProgress(maxval, maxval)
                return None
 
 class MoveHandler(object):
 
-       def __init__(self, tree):
+       def __init__(self, tree, porttree):
                self._tree = tree
-               self._portdir = tree.settings["PORTDIR"]
+               self._portdb = porttree.dbapi
                self._update_keys = ["DEPEND", "RDEPEND", "PDEPEND", "PROVIDE"]
+               self._master_repo = \
+                       self._portdb.getRepositoryName(self._portdb.porttree_root)
 
-       def _grab_global_updates(self, portdir):
+       def _grab_global_updates(self):
                from portage.update import grab_updates, parse_updates
-               updpath = os.path.join(portdir, "profiles", "updates")
-               try:
-                       rawupdates = grab_updates(updpath)
-               except portage.exception.DirectoryNotFound:
-                       rawupdates = []
-               upd_commands = []
+               retupdates = {}
                errors = []
-               for mykey, mystat, mycontent in rawupdates:
-                       commands, errors = parse_updates(mycontent)
-                       upd_commands.extend(commands)
-                       errors.extend(errors)
-               return upd_commands, errors
+
+               for repo_name in self._portdb.getRepositories():
+                       repo = self._portdb.getRepositoryPath(repo_name)
+                       updpath = os.path.join(repo, "profiles", "updates")
+                       if not os.path.isdir(updpath):
+                               continue
+
+                       try:
+                               rawupdates = grab_updates(updpath)
+                       except portage.exception.DirectoryNotFound:
+                               rawupdates = []
+                       upd_commands = []
+                       for mykey, mystat, mycontent in rawupdates:
+                               commands, errors = parse_updates(mycontent)
+                               upd_commands.extend(commands)
+                               errors.extend(errors)
+                       retupdates[repo_name] = upd_commands
+
+               if self._master_repo in retupdates:
+                       retupdates['DEFAULT'] = retupdates[self._master_repo]
+
+               return retupdates, errors
 
        def check(self, onProgress=None):
-               updates, errors = self._grab_global_updates(self._portdir)
+               allupdates, errors = self._grab_global_updates()
                # Matching packages and moving them is relatively fast, so the
                # progress bar is updated in indeterminate mode.
                match = self._tree.dbapi.match
                aux_get = self._tree.dbapi.aux_get
                if onProgress:
                        onProgress(0, 0)
-               for i, update_cmd in enumerate(updates):
-                       if update_cmd[0] == "move":
-                               origcp, newcp = update_cmd[1:]
-                               for cpv in match(origcp):
-                                       errors.append("'%s' moved to '%s'" % (cpv, newcp))
-                       elif update_cmd[0] == "slotmove":
-                               pkg, origslot, newslot = update_cmd[1:]
-                               for cpv in match(pkg):
-                                       slot = aux_get(cpv, ["SLOT"])[0]
-                                       if slot == origslot:
-                                               errors.append("'%s' slot moved from '%s' to '%s'" % \
-                                                       (cpv, origslot, newslot))
-                       if onProgress:
-                               onProgress(0, 0)
+               for repo, updates in allupdates.items():
+                       if repo == 'DEFAULT':
+                               continue
+                       if not updates:
+                               continue
+
+                       def repo_match(repository):
+                               return repository == repo or \
+                                       (repo == self._master_repo and \
+                                       repository not in allupdates)
+
+                       for i, update_cmd in enumerate(updates):
+                               if update_cmd[0] == "move":
+                                       origcp, newcp = update_cmd[1:]
+                                       for cpv in match(origcp):
+                                               if repo_match(aux_get(cpv, ["repository"])[0]):
+                                                       errors.append("'%s' moved to '%s'" % (cpv, newcp))
+                               elif update_cmd[0] == "slotmove":
+                                       pkg, origslot, newslot = update_cmd[1:]
+                                       for cpv in match(pkg):
+                                               slot, prepo = aux_get(cpv, ["SLOT", "repository"])
+                                               if slot == origslot and repo_match(prepo):
+                                                       errors.append("'%s' slot moved from '%s' to '%s'" % \
+                                                               (cpv, origslot, newslot))
+                               if onProgress:
+                                       onProgress(0, 0)
 
                # Searching for updates in all the metadata is relatively slow, so this
                # is where the progress bar comes out of indeterminate mode.
@@ -214,13 +301,22 @@ class MoveHandler(object):
                cpv_all.sort()
                maxval = len(cpv_all)
                aux_update = self._tree.dbapi.aux_update
-               update_keys = self._update_keys
-               from itertools import izip
+               meta_keys = self._update_keys + ['repository']
                from portage.update import update_dbentries
                if onProgress:
                        onProgress(maxval, 0)
                for i, cpv in enumerate(cpv_all):
-                       metadata = dict(izip(update_keys, aux_get(cpv, update_keys)))
+                       metadata = dict(zip(meta_keys, aux_get(cpv, meta_keys)))
+                       repository = metadata.pop('repository')
+                       try:
+                               updates = allupdates[repository]
+                       except KeyError:
+                               try:
+                                       updates = allupdates['DEFAULT']
+                               except KeyError:
+                                       continue
+                       if not updates:
+                               continue
                        metadata_updates = update_dbentries(updates, metadata)
                        if metadata_updates:
                                errors.append("'%s' has outdated metadata" % cpv)
@@ -229,57 +325,58 @@ class MoveHandler(object):
                return errors
 
        def fix(self, onProgress=None):
-               updates, errors = self._grab_global_updates(self._portdir)
+               allupdates, errors = self._grab_global_updates()
                # Matching packages and moving them is relatively fast, so the
                # progress bar is updated in indeterminate mode.
                move = self._tree.dbapi.move_ent
                slotmove = self._tree.dbapi.move_slot_ent
                if onProgress:
                        onProgress(0, 0)
-               for i, update_cmd in enumerate(updates):
-                       if update_cmd[0] == "move":
-                               move(update_cmd)
-                       elif update_cmd[0] == "slotmove":
-                               slotmove(update_cmd)
-                       if onProgress:
-                               onProgress(0, 0)
+               for repo, updates in allupdates.items():
+                       if repo == 'DEFAULT':
+                               continue
+                       if not updates:
+                               continue
+
+                       def repo_match(repository):
+                               return repository == repo or \
+                                       (repo == self._master_repo and \
+                                       repository not in allupdates)
+
+                       for i, update_cmd in enumerate(updates):
+                               if update_cmd[0] == "move":
+                                       move(update_cmd, repo_match=repo_match)
+                               elif update_cmd[0] == "slotmove":
+                                       slotmove(update_cmd, repo_match=repo_match)
+                               if onProgress:
+                                       onProgress(0, 0)
 
                # Searching for updates in all the metadata is relatively slow, so this
                # is where the progress bar comes out of indeterminate mode.
-               cpv_all = self._tree.dbapi.cpv_all()
-               cpv_all.sort()
-               maxval = len(cpv_all)
-               aux_get = self._tree.dbapi.aux_get
-               aux_update = self._tree.dbapi.aux_update
-               update_keys = self._update_keys
-               from itertools import izip
-               from portage.update import update_dbentries
-               if onProgress:
-                       onProgress(maxval, 0)
-               for i, cpv in enumerate(cpv_all):
-                       metadata = dict(izip(update_keys, aux_get(cpv, update_keys)))
-                       metadata_updates = update_dbentries(updates, metadata)
-                       if metadata_updates:
-                               aux_update(cpv, metadata_updates)
-                       if onProgress:
-                               onProgress(maxval, i+1)
+               self._tree.dbapi.update_ents(allupdates, onProgress=onProgress)
                return errors
 
 class MoveInstalled(MoveHandler):
+
+       short_desc = "Perform package move updates for installed packages"
+
        def name():
                return "moveinst"
        name = staticmethod(name)
        def __init__(self):
                myroot = portage.settings["ROOT"]
-               MoveHandler.__init__(self, portage.db[myroot]["vartree"])
+               MoveHandler.__init__(self, portage.db[myroot]["vartree"], portage.db[myroot]["porttree"])
 
 class MoveBinary(MoveHandler):
+
+       short_desc = "Perform package move updates for binary packages"
+
        def name():
                return "movebin"
        name = staticmethod(name)
        def __init__(self):
                myroot = portage.settings["ROOT"]
-               MoveHandler.__init__(self, portage.db[myroot]["bintree"])
+               MoveHandler.__init__(self, portage.db[myroot]["bintree"], portage.db[myroot]["porttree"])
 
 class VdbKeyHandler(object):
        def name():
@@ -334,7 +431,7 @@ class VdbKeyHandler(object):
                                                                keyfile = open(mydir+os.sep+k, "w")
                                                                keyfile.write(s+"\n")
                                                                keyfile.close()
-                                                       except (IOError, OSError), e:
+                                                       except (IOError, OSError) as e:
                                                                errors.append("Could not write %s, reason was: %s" % (mydir+k, e))
                
                return errors
@@ -357,18 +454,74 @@ class ProgressHandler(object):
        def display(self):
                raise NotImplementedError(self)
 
+class CleanResume(object):
+
+       short_desc = "Discard emerge --resume merge lists"
+
+       def name():
+               return "cleanresume"
+       name = staticmethod(name)
+
+       def check(self, onProgress=None):
+               messages = []
+               mtimedb = portage.mtimedb
+               resume_keys = ("resume", "resume_backup")
+               maxval = len(resume_keys)
+               if onProgress:
+                       onProgress(maxval, 0)
+               for i, k in enumerate(resume_keys):
+                       try:
+                               d = mtimedb.get(k)
+                               if d is None:
+                                       continue
+                               if not isinstance(d, dict):
+                                       messages.append("unrecognized resume list: '%s'" % k)
+                                       continue
+                               mergelist = d.get("mergelist")
+                               if mergelist is None or not hasattr(mergelist, "__len__"):
+                                       messages.append("unrecognized resume list: '%s'" % k)
+                                       continue
+                               messages.append("resume list '%s' contains %d packages" % \
+                                       (k, len(mergelist)))
+                       finally:
+                               if onProgress:
+                                       onProgress(maxval, i+1)
+               return messages
+
+       def fix(self, onProgress=None):
+               delete_count = 0
+               mtimedb = portage.mtimedb
+               resume_keys = ("resume", "resume_backup")
+               maxval = len(resume_keys)
+               if onProgress:
+                       onProgress(maxval, 0)
+               for i, k in enumerate(resume_keys):
+                       try:
+                               if mtimedb.pop(k, None) is not None:
+                                       delete_count += 1
+                       finally:
+                               if onProgress:
+                                       onProgress(maxval, i+1)
+               if delete_count:
+                       mtimedb.commit()
+
 def emaint_main(myargv):
 
+       # Similar to emerge, emaint needs a default umask so that created
+       # files (such as the world file) have sane permissions.
+       os.umask(0o22)
+
        # TODO: Create a system that allows external modules to be added without
        #       the need for hard coding.
        modules = {
                "world" : WorldHandler,
                "binhost":BinhostHandler,
                "moveinst":MoveInstalled,
-               "movebin":MoveBinary
+               "movebin":MoveBinary,
+               "cleanresume":CleanResume
        }
 
-       module_names = modules.keys()
+       module_names = list(modules)
        module_names.sort()
        module_names.insert(0, "all")
 
@@ -381,14 +534,22 @@ def emaint_main(myargv):
                setattr(parser, var, str(option))
 
 
-       usage = "usage: emaint [options] " + " | ".join(module_names)
+       usage = "usage: emaint [options] COMMAND"
 
-       usage+= "\n\nCurrently emaint can only check and fix problems with one's world\n"
-       usage+= "file.  Future versions will integrate other portage check-and-fix\n"
-       usage+= "tools and provide a single interface to system health checks."
+       desc = "The emaint program provides an interface to system health " + \
+               "checks and maintenance. See the emaint(1) man page " + \
+               "for additional information about the following commands:"
 
+       usage += "\n\n"
+       for line in textwrap.wrap(desc, 65):
+               usage += "%s\n" % line
+       usage += "\n"
+       usage += "  %s" % "all".ljust(15) + \
+               "Perform all supported commands\n"
+       for m in module_names[1:]:
+               usage += "  %s%s\n" % (m.ljust(15), modules[m].short_desc)
 
-       parser = OptionParser(usage=usage)
+       parser = OptionParser(usage=usage, version=portage.VERSION)
        parser.add_option("-c", "--check", help="check for problems",
                action="callback", callback=exclusive, callback_kwargs={"var":"action"})
        parser.add_option("-f", "--fix", help="attempt to fix problems",
@@ -405,7 +566,7 @@ def emaint_main(myargv):
        if parser.action:
                action = parser.action
        else:
-               print "Defaulting to --check"
+               print("Defaulting to --check")
                action = "-c/--check"
 
        if args[0] == "all":
@@ -421,9 +582,9 @@ def emaint_main(myargv):
                status = "Attempting to fix %s"
                func = "fix"
 
-       isatty = sys.stdout.isatty()
+       isatty = os.environ.get('TERM') != 'dumb' and sys.stdout.isatty()
        for task in tasks:
-               print status % task.name()
+               print(status % task.name())
                inst = task()
                onProgress = None
                if isatty:
@@ -441,14 +602,14 @@ def emaint_main(myargv):
                if isatty:
                        # make sure the final progress is displayed
                        progressHandler.display()
-                       print
+                       print()
                        signal.signal(signal.SIGWINCH, signal.SIG_DFL)
                if result:
-                       print
-                       print "\n".join(result)
-                       print "\n"
+                       print()
+                       print("\n".join(result))
+                       print("\n")
 
-       print "Finished"
+       print("Finished")
 
 if __name__ == "__main__":
        emaint_main(sys.argv[1:])