Merged from trunk -r13355:13366
authorFabian Groffen <grobian@gentoo.org>
Sun, 19 Apr 2009 07:15:02 +0000 (07:15 -0000)
committerFabian Groffen <grobian@gentoo.org>
Sun, 19 Apr 2009 07:15:02 +0000 (07:15 -0000)
   | 13356   | Add support to emerge --metadata for transfering cache from  |
   | zmedico | overlays that have a metadata/cache directory.               |

   | 13357   | Use a new ProfileDesc class to handle the data for each      |
   | zmedico | profile listed in profiles.desc.                             |

   | 13358   | Support use.desc from overlays.                              |
   | zmedico |                                                              |

   | 13359   | Support profiles.desc from overlays.                         |
   | zmedico |                                                              |

   | 13360   | Handle IOError from codecs.open().                           |
   | zmedico |                                                              |

   | 13361   | If a repo doesn't have metadata/layout.conf then use either  |
   | zmedico | missing eclass directory or missing profiles/profiles.desc   |
   |         | to trigger assignment of a default master.                   |

   | 13362   | Fix default master code so that it doesn't trigger when      |
   | zmedico | PORTDIR is empty.                                            |

   | 13363   | Add a new --deselect action which removes atoms from the     |
   | zmedico | world file. This action is implied by uninstall actions,     |
   |         | including --depclean, --prune and --unmerge. Use             |
   |         | --deselect=n in order to prevent uninstall actions from      |
   |         | removing atoms from the world file. This solves bug #259994  |
   |         | and bug #265206.                                             |

   | 13364   | Fix bugs in action_deselect() and add --pretend support.     |
   | zmedico |                                                              |

   | 13365   | Add --ask support to action_deselect().                      |
   | zmedico |                                                              |

   | 13366   | In action_deselect(), use the installed packages to expand   |
   | zmedico | atoms into slot atoms.                                       |

svn path=/main/branches/prefix/; revision=13369

bin/repoman
man/emerge.1
pym/_emerge/__init__.py
pym/_emerge/help.py
pym/portage/dbapi/porttree.py
pym/portage/dep.py

index 5fb6182fc418405224efb503d7700d98c020e07f..3b084d8954cc6553f5da1b166164c71dd2847a80 100755 (executable)
@@ -540,32 +540,105 @@ repodir = os.path.realpath(repodir)
 def caterror(mycat):
        err(mycat+" is not an official category.  Skipping QA checks in this directory.\nPlease ensure that you add "+catdir+" to "+repodir+"/profiles/categories\nif it is a new category.")
 
-# setup a uselist from portage
-uselist=[]
-try:
-       uselist=portage.grabfile(portdir+"/profiles/use.desc")
-       for l in range(0,len(uselist)):
-               uselist[l]=uselist[l].split()[0]
-       for var in repoman_settings["USE_EXPAND"].split():
-               vardescs = portage.grabfile(portdir+"/profiles/desc/"+var.lower()+".desc")
-               for l in range(0, len(vardescs)):
-                       uselist.append(var.lower() + "_" + vardescs[l].split()[0])
-except (IOError, OSError, ParseError), e:
-       logging.exception("Couldn't read USE flags from use.desc")
-       sys.exit(1)
+class ProfileDesc(object):
+       __slots__ = ('abs_path', 'arch', 'status', 'sub_path', 'tree_path',)
+       def __init__(self, arch, status, sub_path, tree_path):
+               self.arch = arch
+               self.status = status
+               self.sub_path = normalize_path(sub_path.lstrip(os.sep))
+               self.tree_path = tree_path
+               self.abs_path = os.path.join(tree_path, 'profiles', self.sub_path)
+
+profile_list = []
+valid_profile_types = frozenset(['dev', 'exp', 'stable'])
+
+# get lists of valid keywords, licenses, and use
+kwlist = set()
+liclist = set()
+uselist = set()
+
+for path in portdb.porttrees:
+       try:
+               liclist.update(os.listdir(os.path.join(path, "licenses")))
+       except OSError:
+               pass
+       kwlist.update(portage.grabfile(os.path.join(path,
+               "profiles", "arch.list")))
+
+       use_desc = portage.grabfile(os.path.join(path, 'profiles', 'use.desc'))
+       for x in use_desc:
+               x = x.split()
+               if x:
+                       uselist.add(x[0])
+
+       expand_desc_dir = os.path.join(path, 'profiles', 'desc')
+       try:
+               expand_list = os.listdir(expand_desc_dir)
+       except OSError:
+               pass
+       else:
+               for fn in expand_list:
+                       if not fn[-5:] == '.desc':
+                               continue
+                       use_prefix = fn[:-5].lower() + '_'
+                       for x in portage.grabfile(os.path.join(expand_desc_dir, fn)):
+                               x = x.split()
+                               if x:
+                                       uselist.add(use_prefix + x[0])
 
-# get lists of valid keywords and licenses
-kwlist = set(portage.grabfile(os.path.join(portdir, "profiles", "arch.list")))
-liclist = set(portage.listdir(os.path.join(portdir, "licenses")))
+       desc_path = os.path.join(path, 'profiles', 'profiles.desc')
+       try:
+               desc_file = codecs.open(desc_path, mode='r', errors='replace')
+       except EnvironmentError:
+               pass
+       else:
+               for i, x in enumerate(desc_file):
+                       if x[0] == "#":
+                               continue
+                       arch = x.split()
+                       if len(arch) == 0:
+                               continue
+                       if len(arch) != 3:
+                               err("wrong format: \"" + bad(x.strip()) + "\" in " + \
+                                       desc_path + " line %d" % (i+1, ))
+                       elif arch[0] not in kwlist:
+                               err("invalid arch: \"" + bad(arch[0]) + "\" in " + \
+                                       desc_path + " line %d" % (i+1, ))
+                       elif arch[2] not in valid_profile_types:
+                               err("invalid profile type: \"" + bad(arch[2]) + "\" in " + \
+                                       desc_path + " line %d" % (i+1, ))
+                       profile_desc = ProfileDesc(arch[0], arch[2], arch[1], portdir)
+                       if not os.path.isdir(profile_desc.abs_path):
+                               logging.error(
+                                       "Invalid %s profile (%s) for arch %s in %s line %d",
+                                       arch[2], arch[1], arch[0], desc_path, i+1)
+                               continue
+                       profile_list.append(profile_desc)
+               desc_file.close()
+
+repoman_settings['PORTAGE_ARCHLIST'] = ' '.join(sorted(kwlist))
+repoman_settings.backup_changes('PORTAGE_ARCHLIST')
+
+# Ensure that profile sub_path attributes are unique. Process in reverse order
+# so that profiles with duplicate sub_path from overlays will override
+# profiles with the same sub_path from parent repos.
+profiles = {}
+profile_list.reverse()
+profile_sub_paths = set()
+for prof in profile_list:
+       if prof.sub_path in profile_sub_paths:
+               continue
+       profile_sub_paths.add(prof.sub_path)
+       profiles.setdefault(prof.arch, []).append(prof)
 
-if portdir_overlay != portdir:
-       for porttree in portdb.porttrees[1:]:
-               try:
-                       liclist.update(os.listdir(os.path.join(porttree, "licenses")))
-               except OSError:
-                       pass
-               kwlist.update(portage.grabfile(os.path.join(porttree,
-                       "profiles", "arch.list")))
+for x in repoman_settings.archlist():
+       if x[0] == "~":
+               continue
+       if x not in profiles:
+               print red("\""+x+"\" doesn't have a valid profile listed in profiles.desc.")
+               print red("You need to either \"cvs update\" your profiles dir or follow this")
+               print red("up with the "+x+" team.")
+               print
 
 if not liclist:
        logging.fatal("Couldn't find licenses?")
@@ -575,6 +648,10 @@ if not kwlist:
        logging.fatal("Couldn't read KEYWORDS from arch.list")
        sys.exit(1)
 
+if not uselist:
+       logging.fatal("Couldn't find use.desc?")
+       sys.exit(1)
+
 scanlist=[]
 if repolevel==2:
        #we are inside a category directory
@@ -609,47 +686,6 @@ scanlist.sort()
 
 logging.debug("Found the following packages to scan:\n%s" % '\n'.join(scanlist))
 
-profiles={}
-valid_profile_types = frozenset(["dev", "exp", "stable"])
-descfile=portdir+"/profiles/profiles.desc"
-if os.path.exists(descfile):
-       for i, x in enumerate(open(descfile)):
-               if x[0]=="#":
-                       continue
-               arch=x.split()
-               if len(arch) == 0:
-                       continue
-               if len(arch)!=3:
-                       err("wrong format: \"" + bad(x.strip()) + "\" in " + \
-                               descfile + " line %d" % (i+1, ))
-               elif arch[0] not in kwlist:
-                       err("invalid arch: \"" + bad(arch[0]) + "\" in " + \
-                               descfile + " line %d" % (i+1, ))
-               elif arch[2] not in valid_profile_types:
-                       err("invalid profile type: \"" + bad(arch[2]) + "\" in " + \
-                               descfile + " line %d" % (i+1, ))
-               if not os.path.isdir(portdir+"/profiles/"+arch[1]):
-                       print "Invalid "+arch[2]+" profile ("+arch[1]+") for arch "+arch[0]
-                       continue
-               if arch[0] in profiles:
-                       profiles[arch[0]]+= [[arch[1], arch[2]]]
-               else:
-                       profiles[arch[0]] = [[arch[1], arch[2]]]
-
-       for x in repoman_settings.archlist():
-               if x[0] == "~":
-                       continue
-               if x not in profiles:
-                       print red("\""+x+"\" doesn't have a valid profile listed in profiles.desc.")
-                       print red("You need to either \"cvs update\" your profiles dir or follow this")
-                       print red("up with the "+x+" team.")
-                       print
-else:
-       print red("profiles.desc does not exist: "+descfile)
-       print red("You need to do \"cvs update\" in profiles dir.")
-       print
-       sys.exit(1)
-
 def dev_keywords(profiles):
        """
        Create a set of KEYWORDS values that exist in 'dev'
@@ -659,11 +695,11 @@ def dev_keywords(profiles):
        """
        type_arch_map = {}
        for arch, arch_profiles in profiles.iteritems():
-               for profile_path, profile_type in arch_profiles:
-                       arch_set = type_arch_map.get(profile_type)
+               for prof in arch_profiles:
+                       arch_set = type_arch_map.get(prof.status)
                        if arch_set is None:
                                arch_set = set()
-                               type_arch_map[profile_type] = arch_set
+                               type_arch_map[prof.status] = arch_set
                        arch_set.add(arch)
 
        dev_keywords = type_arch_map.get('dev', set())
@@ -1524,23 +1560,20 @@ for x in scanlist:
                                
                        for prof in profiles[arch]:
 
-                               if prof[1] not in ("stable", "dev") or \
-                                       prof[1] == "dev" and not options.include_dev:
+                               if prof.status not in ("stable", "dev") or \
+                                       prof.status == "dev" and not options.include_dev:
                                        continue
 
-                               profdir = portdir+"/profiles/"+prof[0]
-       
-                               if prof[0] in arch_caches:
-                                       dep_settings = arch_caches[prof[0]]
-                               else:
+                               dep_settings = arch_caches.get(prof.sub_path)
+                               if dep_settings is None:
                                        dep_settings = portage.config(
-                                               config_profile_path=profdir,
+                                               config_profile_path=prof.abs_path,
                                                config_incrementals=portage.const.INCREMENTALS,
                                                local_config=False,
                                                env=env)
                                        if options.without_mask:
                                                dep_settings.pmaskdict.clear()
-                                       arch_caches[prof[0]] = dep_settings
+                                       arch_caches[prof.sub_path] = dep_settings
                                        while True:
                                                try:
                                                        # Protect ACCEPT_KEYWORDS from config.regenerate()
@@ -1549,7 +1582,7 @@ for x in scanlist:
                                                except ValueError:
                                                        break
 
-                               xmatch_cache_key = (prof[0], tuple(groups))
+                               xmatch_cache_key = (prof.sub_path, tuple(groups))
                                xcache = arch_xmatch_caches.get(xmatch_cache_key)
                                if xcache is None:
                                        portdb.melt()
@@ -1571,7 +1604,8 @@ for x in scanlist:
                                        prov_cp = portage.dep_getkey(myprovide)
                                        if prov_cp not in dep_settings.getvirtuals():
                                                stats["virtual.unavailable"]+=1
-                                               fails["virtual.unavailable"].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+prov_cp)
+                                               fails["virtual.unavailable"].append("%s: %s(%s) %s" % \
+                                                       (relative_path, keyword, prof.sub_path, prov_cp))
 
                                if not baddepsyntax:
                                        ismasked = os.path.join(catdir, y) not in \
@@ -1593,7 +1627,7 @@ for x in scanlist:
                                                have_dev_keywords = \
                                                        bool(dev_keywords.intersection(keywords))
 
-                                       if prof[1] == "dev":
+                                       if prof.status == "dev":
                                                suffix=suffix+"indev"
 
                                        for mytype,mypos in [["DEPEND",len(missingvars)],["RDEPEND",len(missingvars)+1],["PDEPEND",len(missingvars)+2]]:
@@ -1602,33 +1636,28 @@ for x in scanlist:
                                                myvalue = myaux[mytype]
                                                if not myvalue:
                                                        continue
-                                               try:
-                                                       mydep = portage.dep_check(myvalue, portdb,
-                                                               dep_settings, use="all", mode=matchmode,
-                                                               trees=trees)
-                                               except KeyError, e:
-                                                       stats[mykey]=stats[mykey]+1
-                                                       fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(e[0]))
-                                                       continue
-       
-                                               if mydep[0]==1:
-                                                       if mydep[1]!=[]:
+
+                                               success, atoms = portage.dep_check(myvalue, portdb,
+                                                       dep_settings, use="all", mode=matchmode,
+                                                       trees=trees)
+
+                                               if success:
+                                                       if atoms:
                                                                #we have some unsolvable deps
                                                                #remove ! deps, which always show up as unsatisfiable
-                                                               d=0
-                                                               while d<len(mydep[1]):
-                                                                       if mydep[1][d][0]=="!":
-                                                                               del mydep[1][d]
-                                                                       else:
-                                                                               d += 1
+                                                               atoms = [str(atom) for atom in atoms if not atom.blocker]
                                                                #if we emptied out our list, continue:
-                                                               if not mydep[1]:
+                                                               if not atoms:
                                                                        continue
                                                                stats[mykey]=stats[mykey]+1
-                                                               fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(mydep[1]))
+                                                               fails[mykey].append("%s: %s(%s) %s" % \
+                                                                       (relative_path, keyword,
+                                                                       prof.sub_path, repr(atoms)))
                                                else:
                                                        stats[mykey]=stats[mykey]+1
-                                                       fails[mykey].append(x+"/"+y+".ebuild: "+keyword+"("+prof[0]+") "+repr(mydep[1]))
+                                                       fails[mykey].append("%s: %s(%s) %s" % \
+                                                               (relative_path, keyword,
+                                                               prof.sub_path, repr(atoms)))
 
        # Check for 'all unstable' or 'all masked' -- ACCEPT_KEYWORDS is stripped
        # XXX -- Needs to be implemented in dep code. Can't determine ~arch nicely.
index 9dc53d269f36e399c55b60dc8fc1a1ffd22dc3f6..4904426541535ad54ea74cb9d6625d47afbbe3af 100644 (file)
@@ -130,6 +130,13 @@ unmerge matched packages that have no reverse dependencies. Use
 \fB\-\-depclean\fR together with \fB\-\-verbose\fR to show reverse
 dependencies.
 .TP
+.BR "\-\-deselect[=n]"
+Remove atoms from the world file. This action is implied
+by uninstall actions, including \fB-\-depclean\fR,
+\fB-\-prune\fR and \fB-\-unmerge\fR. Use \fB-\-deselect=n\fR
+in order to prevent uninstall actions from removing
+atoms from the world file.
+.TP
 .BR "\-\-help " (\fB\-h\fR)
 Displays help information for emerge.  Adding one of the additional
 arguments listed above will give you more specific help information
index 6ea7897f2e900291644e3825df77007c1090ef70..a356b11aecbd145c3065e6f3ed44c599f1ffcf12 100644 (file)
@@ -63,6 +63,7 @@ portage.dep._dep_check_strict = True
 import portage.util
 import portage.locks
 import portage.exception
+from portage.cache.cache_errors import CacheError
 from portage.const import EPREFIX, BPREFIX, EPREFIX_LSTRIP
 from portage.data import secpass
 from portage.elog.messages import eerror
@@ -10106,6 +10107,24 @@ class JobStatusDisplay(object):
 
                xtermTitle(" ".join(plain_output.split()))
 
+class ProgressHandler(object):
+       def __init__(self):
+               self.curval = 0
+               self.maxval = 0
+               self._last_update = 0
+               self.min_latency = 0.2
+
+       def onProgress(self, maxval, curval):
+               self.maxval = maxval
+               self.curval = curval
+               cur_time = time.time()
+               if cur_time - self._last_update >= self.min_latency:
+                       self._last_update = cur_time
+                       self.display()
+
+       def display(self):
+               raise NotImplementedError(self)
+
 class Scheduler(PollScheduler):
 
        _opts_ignore_blockers = \
@@ -11871,6 +11890,8 @@ def unmerge(root_config, myopts, unmerge_action,
        clean_world=1, clean_delay=1, ordered=0, raise_on_error=0,
        scheduler=None, writemsg_level=portage.util.writemsg_level):
 
+       if clean_world:
+               clean_world = myopts.get('--deselect') != 'n'
        quiet = "--quiet" in myopts
        settings = root_config.settings
        sets = root_config.sets
@@ -12780,7 +12801,8 @@ def show_mask_docs():
 def action_sync(settings, trees, mtimedb, myopts, myaction):
        xterm_titles = "notitles" not in settings.features
        emergelog(xterm_titles, " === sync")
-       myportdir = settings.get("PORTDIR", None)
+       portdb = trees[settings["ROOT"]]["porttree"].dbapi
+       myportdir = portdb.porttree_root
        out = portage.output.EOutput()
        if not myportdir:
                sys.stderr.write("!!! PORTDIR is undefined.  Is /etc/make.globals missing?\n")
@@ -13285,8 +13307,12 @@ def action_sync(settings, trees, mtimedb, myopts, myaction):
        root_config = trees[settings["ROOT"]]["root_config"]
        portdb = trees[settings["ROOT"]]["porttree"].dbapi
 
-       if os.path.exists(myportdir+"/metadata/cache") and updatecache_flg:
-               action_metadata(settings, portdb, myopts)
+       if updatecache_flg and \
+               os.path.exists(os.path.join(myportdir, 'metadata', 'cache')):
+
+               # Only update cache for myportdir since that's
+               # the only one that's been synced here.
+               action_metadata(settings, portdb, myopts, porttrees=[myportdir])
 
        if portage._global_updates(trees, mtimedb["updates"]):
                mtimedb.commit()
@@ -13454,8 +13480,10 @@ def git_sync_timestamps(settings, portdir):
 
        return os.EX_OK
 
-def action_metadata(settings, portdb, myopts):
-       portage.writemsg_stdout("\n>>> Updating Portage cache:      ")
+def action_metadata(settings, portdb, myopts, porttrees=None):
+       if porttrees is None:
+               porttrees = portdb.porttrees
+       portage.writemsg_stdout("\n>>> Updating Portage cache\n")
        old_umask = os.umask(0002)
        cachedir = os.path.normpath(settings.depcachedir)
        if cachedir in ["/",    "/bin", "/dev",  "/etc",  "/home",
@@ -13467,60 +13495,213 @@ def action_metadata(settings, portdb, myopts):
                        "!!! This is ALMOST CERTAINLY NOT what you want: '%s'" % cachedir
                sys.exit(73)
        if not os.path.exists(cachedir):
-               os.mkdir(cachedir)
-
-       ec = portage.eclass_cache.cache(portdb.porttree_root)
-       myportdir = os.path.realpath(settings["PORTDIR"])
-       cm = settings.load_best_module("portdbapi.metadbmodule")(
-               myportdir, "metadata/cache", portage.auxdbkeys[:], readonly=True)
-
-       from portage.cache import util
-
-       class percentage_noise_maker(util.quiet_mirroring):
-               def __init__(self, dbapi):
-                       self.dbapi = dbapi
-                       self.cp_all = dbapi.cp_all()
-                       l = len(self.cp_all)
-                       self.call_update_min = 100000000
-                       self.min_cp_all = l/100.0
-                       self.count = 1
-                       self.pstr = ''
-
-               def __iter__(self):
-                       for x in self.cp_all:
-                               self.count += 1
-                               if self.count > self.min_cp_all:
-                                       self.call_update_min = 0
-                                       self.count = 0
-                               for y in self.dbapi.cp_list(x):
-                                       yield y
-                       self.call_update_mine = 0
-
-               def update(self, *arg):
+               os.makedirs(cachedir)
+
+       auxdbkeys = [x for x in portage.auxdbkeys if not x.startswith("UNUSED_0")]
+       auxdbkeys = tuple(auxdbkeys)
+
+       class TreeData(object):
+               __slots__ = ('dest_db', 'eclass_db', 'path', 'src_db', 'valid_nodes')
+               def __init__(self, dest_db, eclass_db, path, src_db):
+                       self.dest_db = dest_db
+                       self.eclass_db = eclass_db
+                       self.path = path
+                       self.src_db = src_db
+                       self.valid_nodes = set()
+
+       porttrees_data = []
+       for path in porttrees:
+               src_db = portdb._pregen_auxdb.get(path)
+               if src_db is None and \
+                       os.path.isdir(os.path.join(path, 'metadata', 'cache')):
+                       src_db = portdb.metadbmodule(
+                               path, 'metadata/cache', auxdbkeys, readonly=True)
                        try:
-                               self.pstr = int(self.pstr) + 1
-                       except ValueError:
-                               self.pstr = 1
-                       sys.stdout.write("%s%i%%" % \
-                               ("\b" * (len(str(self.pstr))+1), self.pstr))
-                       sys.stdout.flush()
-                       self.call_update_min = 10000000
+                               src_db.ec = portdb._repo_info[path].eclass_db
+                       except AttributeError:
+                               pass
 
-               def finish(self, *arg):
-                       sys.stdout.write("\b\b\b\b100%\n")
-                       sys.stdout.flush()
+               if src_db is not None:
+                       porttrees_data.append(TreeData(portdb.auxdb[path],
+                               portdb._repo_info[path].eclass_db, path, src_db))
+
+       porttrees = [tree_data.path for tree_data in porttrees_data]
+
+       isatty = sys.stdout.isatty()
+       quiet = not isatty or '--quiet' in myopts
+       onProgress = None
+       if not quiet:
+               progressBar = portage.output.TermProgressBar()
+               progressHandler = ProgressHandler()
+               onProgress = progressHandler.onProgress
+               def display():
+                       progressBar.set(progressHandler.curval, progressHandler.maxval)
+               progressHandler.display = display
+               def sigwinch_handler(signum, frame):
+                       lines, progressBar.term_columns = \
+                               portage.output.get_term_size()
+               signal.signal(signal.SIGWINCH, sigwinch_handler)
+
+       # Temporarily override portdb.porttrees so portdb.cp_all()
+       # will only return the relevant subset.
+       portdb_porttrees = portdb.porttrees
+       portdb.porttrees = porttrees
+       try:
+               cp_all = portdb.cp_all()
+       finally:
+               portdb.porttrees = portdb_porttrees
+
+       curval = 0
+       maxval = len(cp_all)
+       if onProgress is not None:
+               onProgress(maxval, curval)
+
+       from portage.cache.util import quiet_mirroring
+       from portage import eapi_is_supported, \
+               _validate_cache_for_unsupported_eapis
+
+       # TODO: Display error messages, but do not interfere with the progress bar.
+       # Here's how:
+       #  1) erase the progress bar
+       #  2) show the error message
+       #  3) redraw the progress bar on a new line
+       noise = quiet_mirroring()
+
+       for cp in cp_all:
+               for tree_data in porttrees_data:
+                       for cpv in portdb.cp_list(cp, mytree=tree_data.path):
+                               tree_data.valid_nodes.add(cpv)
+                               try:
+                                       src = tree_data.src_db[cpv]
+                               except KeyError, e:
+                                       noise.missing_entry(cpv)
+                                       del e
+                                       continue
+                               except CacheError, ce:
+                                       noise.exception(cpv, ce)
+                                       del ce
+                                       continue
 
-       if "--quiet" in myopts:
-               def quicky_cpv_generator(cp_all_list):
-                       for x in cp_all_list:
-                               for y in portdb.cp_list(x):
-                                       yield y
-               source = quicky_cpv_generator(portdb.cp_all())
-               noise_maker = portage.cache.util.quiet_mirroring()
-       else:
-               noise_maker = source = percentage_noise_maker(portdb)
-       portage.cache.util.mirror_cache(source, cm, portdb.auxdb[myportdir],
-               eclass_cache=ec, verbose_instance=noise_maker)
+                               eapi = src.get('EAPI')
+                               if not eapi:
+                                       eapi = '0'
+                               eapi = eapi.lstrip('-')
+                               eapi_supported = eapi_is_supported(eapi)
+                               if not eapi_supported:
+                                       if not _validate_cache_for_unsupported_eapis:
+                                               noise.misc(cpv, "unable to validate " + \
+                                                       "cache for EAPI='%s'" % eapi)
+                                               continue
+
+                               dest = None
+                               try:
+                                       dest = tree_data.dest_db[cpv]
+                               except (KeyError, CacheError):
+                                       pass
+
+                               for d in (src, dest):
+                                       if d is not None and d.get('EAPI') in ('', '0'):
+                                               del d['EAPI']
+
+                               if dest is not None:
+                                       if not (dest['_mtime_'] == src['_mtime_'] and \
+                                               tree_data.eclass_db.is_eclass_data_valid(
+                                                       dest['_eclasses_']) and \
+                                               set(dest['_eclasses_']) == set(src['_eclasses_'])):
+                                               dest = None
+                                       else:
+                                               # We don't want to skip the write unless we're really
+                                               # sure that the existing cache is identical, so don't
+                                               # trust _mtime_ and _eclasses_ alone.
+                                               for k in set(chain(src, dest)).difference(
+                                                       ('_mtime_', '_eclasses_')):
+                                                       if dest.get(k, '') != src.get(k, ''):
+                                                               dest = None
+                                                               break
+
+                               if dest is not None:
+                                       # The existing data is valid and identical,
+                                       # so there's no need to overwrite it.
+                                       continue
+
+                               try:
+                                       inherited = src.get('INHERITED', '')
+                                       eclasses = src.get('_eclasses_')
+                               except CacheError, ce:
+                                       noise.exception(cpv, ce)
+                                       del ce
+                                       continue
+
+                               if eclasses is not None:
+                                       if not tree_data.eclass_db.is_eclass_data_valid(
+                                               src['_eclasses_']):
+                                               noise.eclass_stale(cpv)
+                                               continue
+                                       inherited = eclasses
+                               else:
+                                       inherited = inherited.split()
+
+                               if inherited:
+                                       if tree_data.src_db.complete_eclass_entries and \
+                                               eclasses is None:
+                                               noise.corruption(cpv, "missing _eclasses_ field")
+                                               continue
+
+                                       # Even if _eclasses_ already exists, replace it with data from
+                                       # eclass_cache, in order to insert local eclass paths.
+                                       try:
+                                               eclasses = tree_data.eclass_db.get_eclass_data(inherited)
+                                       except KeyError:
+                                               # INHERITED contains a non-existent eclass.
+                                               noise.eclass_stale(cpv)
+                                               continue
+
+                                       if eclasses is None:
+                                               noise.eclass_stale(cpv)
+                                               continue
+                                       src['_eclasses_'] = eclasses
+
+                               if not eapi_supported:
+                                       src = {
+                                               'EAPI'       : '-' + eapi,
+                                               '_mtime_'    : src['_mtime_'],
+                                               '_eclasses_' : src['_eclasses_'],
+                                       }
+
+                               try:
+                                       tree_data.dest_db[cpv] = src
+                               except CacheError, ce:
+                                       noise.exception(cpv, ce)
+                                       del ce
+
+               curval += 1
+               if onProgress is not None:
+                       onProgress(maxval, curval)
+
+       if onProgress is not None:
+               onProgress(maxval, curval)
+
+       for tree_data in porttrees_data:
+               try:
+                       dead_nodes = set(tree_data.dest_db.iterkeys())
+               except CacheError, e:
+                       writemsg_level("Error listing cache entries for " + \
+                               "'%s': %s, continuing...\n" % (tree_data.path, e),
+                               level=logging.ERROR, noiselevel=-1)
+                       del e
+               else:
+                       dead_nodes.difference_update(tree_data.valid_nodes)
+                       for cpv in dead_nodes:
+                               try:
+                                       tree_data.dest_db[cpv]
+                               except (KeyError, CacheError):
+                                       pass
+
+       if not quiet:
+               # make sure the final progress is displayed
+               progressHandler.display()
+               print
+               signal.signal(signal.SIGWINCH, signal.SIG_DFL)
 
        sys.stdout.flush()
        os.umask(old_umask)
@@ -13955,12 +14136,71 @@ def action_uninstall(settings, trees, ldpath_mtimes,
                unmerge(trees[settings["ROOT"]]['root_config'], opts, action,
                        valid_atoms, ldpath_mtimes, ordered=ordered)
                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)
 
        return rval
 
+def action_deselect(settings, trees, opts, atoms):
+       root_config = trees[settings['ROOT']]['root_config']
+       world_set = root_config.sets['world']
+       if not hasattr(world_set, 'update'):
+               writemsg_level("World set does not appear to be mutable.\n",
+                       level=logging.ERROR, noiselevel=-1)
+               return 1
+
+       vardb = root_config.trees['vartree'].dbapi
+       expanded_atoms = set(atoms)
+       from portage.dep import Atom
+       for atom in atoms:
+               for cpv in vardb.match(atom):
+                       slot, = vardb.aux_get(cpv, ['SLOT'])
+                       if not slot:
+                               slot = '0'
+                       expanded_atoms.add(Atom('%s:%s' % (portage.cpv_getkey(cpv), slot)))
+
+       pretend = '--pretend' in opts
+       locked = False
+       if not pretend and hasattr(world_set, 'lock'):
+               world_set.lock()
+               locked = True
+       try:
+               discard_atoms = set()
+               world_set.load()
+               for atom in world_set:
+                       if not isinstance(atom, Atom):
+                               # nested set
+                               continue
+                       for arg_atom in expanded_atoms:
+                               if arg_atom.intersects(atom) and \
+                                       not (arg_atom.slot and not atom.slot):
+                                       discard_atoms.add(atom)
+                                       break
+               if discard_atoms:
+                       for atom in sorted(discard_atoms):
+                               print ">>> Removing %s from \"world\" favorites file..." % \
+                                       colorize("INFORM", str(atom))
+
+                       if '--ask' in opts:
+                               prompt = "Would you like to remove these " + \
+                                       "packages from your world favorites?"
+                               if userquery(prompt) == 'No':
+                                       return os.EX_OK
+
+                       remaining = set(world_set)
+                       remaining.difference_update(discard_atoms)
+                       if not pretend:
+                               world_set.replace(remaining)
+               else:
+                       print ">>> No matching atoms found in \"world\" favorites file..."
+       finally:
+               if locked:
+                       world_set.unlock()
+       return os.EX_OK
+
 def action_depclean(settings, trees, ldpath_mtimes,
        myopts, action, myfiles, spinner):
        # Kill packages that aren't explicitly merged or are required as a
@@ -13999,6 +14239,7 @@ def action_depclean(settings, trees, ldpath_mtimes,
        root_config = trees[myroot]["root_config"]
        getSetAtoms = root_config.setconfig.getSetAtoms
        vardb = trees[myroot]["vartree"].dbapi
+       deselect = myopts.get('--deselect') != 'n'
 
        required_set_names = ("system", "world")
        required_sets = {}
@@ -14056,11 +14297,13 @@ def action_depclean(settings, trees, ldpath_mtimes,
        if action == "depclean":
 
                if args_set:
+
+                       if deselect:
+                               world_temp_set.clear()
+
                        # Pull in everything that's installed but not matched
                        # by an argument atom since we don't want to clean any
                        # package if something depends on it.
-
-                       world_temp_set.clear()
                        for pkg in vardb:
                                spinner.update()
 
@@ -14077,9 +14320,11 @@ def action_depclean(settings, trees, ldpath_mtimes,
 
        elif action == "prune":
 
+               if deselect:
+                       world_temp_set.clear()
+
                # Pull in everything that's installed since we don't
                # to prune a package if something depends on it.
-               world_temp_set.clear()
                world_temp_set.update(vardb.cp_all())
 
                if not args_set:
@@ -15081,16 +15326,19 @@ def insert_optional_args(args):
 
        new_args = []
        jobs_opts = ("-j", "--jobs")
-       root_deps_opt = '--root-deps'
-       root_deps_choices = ('True', 'rdeps')
+       default_arg_opts = {
+               '--deselect'   : ('n',),
+               '--root-deps'  : ('rdeps',),
+       }
        arg_stack = args[:]
        arg_stack.reverse()
        while arg_stack:
                arg = arg_stack.pop()
 
-               if arg == root_deps_opt:
+               default_arg_choices = default_arg_opts.get(arg)
+               if default_arg_choices is not None:
                        new_args.append(arg)
-                       if arg_stack and arg_stack[-1] in root_deps_choices:
+                       if arg_stack and arg_stack[-1] in default_arg_choices:
                                new_args.append(arg_stack.pop())
                        else:
                                # insert default argument
@@ -15158,6 +15406,12 @@ def parse_opts(tmpcmdline, silent=False):
                        "choices":("y", "n")
                },
 
+               "--deselect": {
+                       "help"    : "remove atoms from the world file",
+                       "type"    : "choice",
+                       "choices" : ("True", "n")
+               },
+
                "--jobs": {
 
                        "help"   : "Specifies the number of packages to build " + \
@@ -15223,6 +15477,9 @@ def parse_opts(tmpcmdline, silent=False):
 
        myoptions, myargs = parser.parse_args(args=tmpcmdline)
 
+       if myoptions.deselect == "True":
+               myoptions.deselect = True
+
        if myoptions.root_deps == "True":
                myoptions.root_deps = True
 
@@ -15280,6 +15537,9 @@ def parse_opts(tmpcmdline, silent=False):
                                sys.exit(1)
                        myaction = action_opt
 
+       if myaction is None and myoptions.deselect is True:
+               myaction = 'deselect'
+
        myfiles += myargs
 
        return myaction, myopts, myfiles
@@ -15948,7 +16208,7 @@ def emerge_main():
        if portage.secpass < 2:
                # We've already allowed "--version" and "--help" above.
                if "--pretend" not in myopts and myaction not in ("search","info"):
-                       need_superuser = not \
+                       need_superuser = myaction in ('deselect',) or not \
                                (fetchonly or \
                                (buildpkgonly and secpass >= 1) or \
                                myaction in ("metadata", "regen") or \
@@ -16046,11 +16306,11 @@ def emerge_main():
                action_search(trees[settings["ROOT"]]["root_config"],
                        myopts, myfiles, spinner)
 
-       elif myaction in ('clean', 'depclean', 'prune', 'unmerge'):
+       elif myaction in ('clean', 'depclean', 'deselect', 'prune', 'unmerge'):
                validate_ebuild_environment(trees)
                rval = action_uninstall(settings, trees, mtimedb["ldpath"],
                        myopts, myaction, myfiles, spinner)
-               if not (buildpkgonly or fetchonly or pretend):
+               if not (myaction == 'deselect' or buildpkgonly or fetchonly or pretend):
                        post_emerge(root_config, myopts, mtimedb, rval)
                return rval
 
index fd57cc3bec41a98ce0dedfe1620695476dea8cab..4afece3a3a63b6e7faa0684f3d1c9a7debe32d09 100644 (file)
@@ -103,6 +103,18 @@ def help(myaction,myopts,havecolor=1):
                        "matched packages that have no reverse dependencies. Use " + \
                        "--depclean together with --verbose to show reverse dependencies."
 
+               for line in wrap(paragraph, desc_width):
+                       print desc_indent + line
+               print
+               print "       " + green("--deselect") + "[=%s]" % turquoise("n")
+
+               paragraph = \
+                       "Remove atoms from the world file. This action is implied " + \
+                       "by uninstall actions, including --depclean, " + \
+                       "--prune and --unmerge. Use --deselect=n " + \
+                       "in order to prevent uninstall actions from removing " + \
+                       "atoms from the world file."
+
                for line in wrap(paragraph, desc_width):
                        print desc_indent + line
                print
index c07cf4ff264b2617a4531c51c2aeaf3d560c6545..318d00c112baf4375562800d4e39b0c87b68dde2 100644 (file)
@@ -253,11 +253,13 @@ class portdbapi(dbapi):
                                else:
                                        porttrees.append(master_path)
 
-                       if not porttrees:
-                               # Make PORTDIR the default master, but only if this
-                               # repo doesn't provide profiles.desc itself.
+                       if not porttrees and path != porttree_root:
+                               # Make PORTDIR the default master, but only if our
+                               # heuristics suggest that it's necessary.
                                profiles_desc = os.path.join(path, 'profiles', 'profiles.desc')
-                               if not os.path.exists(profiles_desc):
+                               eclass_dir = os.path.join(path, 'eclass')
+                               if not os.path.isfile(profiles_desc) or \
+                                       not os.path.isdir(eclass_dir):
                                        porttrees.append(porttree_root)
 
                        porttrees.append(path)
index 86625871511cae5dddd3ad622e9a726c540101cc..80ebdadbcdae463140b882e11d8296672dabd506 100644 (file)
@@ -558,6 +558,36 @@ class Atom(object):
                raise AttributeError("Atom instances are immutable",
                        self.__class__, name, value)
 
+       def intersects(self, other):
+               """
+               Atoms with different operator or cpv attributes cause this method to
+               return False. TODO: Detect intersection when operators are present.
+               @param other: The package atom to match
+               @type other: Atom
+               @rtype: Boolean
+               @return: True if this atom and the other atom intersect,
+                       False otherwise.
+               """
+               if not isinstance(other, Atom):
+                       raise TypeError("expected %s, got %s" % \
+                               (Atom, type(other)))
+
+               if self == other:
+                       return True
+
+               if self.cp != other.cp or \
+                       self.use != other.use or \
+                       self.operator != other.operator or \
+                       self.cpv != other.cpv:
+                       return False
+
+               if self.slot is None or \
+                       other.slot is None or \
+                       self.slot == other.slot:
+                       return True
+
+               return False
+
        # Implement some common str methods.
 
        def __eq__(self, other):