| # Copyright 1999-2020 Gentoo Authors |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| import logging |
| import signal |
| import sys |
| import textwrap |
| import portage |
| from portage import os |
| from portage.dbapi._expand_new_virt import expand_new_virt |
| from portage.localization import _ |
| from portage.output import bold, colorize, darkgreen, green |
| from portage._sets import SETPREFIX |
| from portage._sets.base import EditablePackageSet |
| from portage.versions import cpv_sort_key, _pkg_str |
| |
| from _emerge.emergelog import emergelog |
| from _emerge.Package import Package |
| from _emerge.UserQuery import UserQuery |
| from _emerge.UninstallFailure import UninstallFailure |
| from _emerge.countdown import countdown |
| |
| |
| def _unmerge_display( |
| root_config, |
| myopts, |
| unmerge_action, |
| unmerge_files, |
| clean_delay=1, |
| ordered=0, |
| writemsg_level=portage.util.writemsg_level, |
| ): |
| """ |
| Returns a tuple of (returncode, pkgmap) where returncode is |
| os.EX_OK if no errors occur, and 1 otherwise. |
| """ |
| |
| quiet = "--quiet" in myopts |
| settings = root_config.settings |
| sets = root_config.sets |
| vartree = root_config.trees["vartree"] |
| candidate_catpkgs = [] |
| global_unmerge = 0 |
| out = portage.output.EOutput() |
| pkg_cache = {} |
| db_keys = list(vartree.dbapi._aux_cache_keys) |
| |
| def _pkg(cpv): |
| pkg = pkg_cache.get(cpv) |
| if pkg is None: |
| pkg = Package( |
| built=True, |
| cpv=cpv, |
| installed=True, |
| metadata=zip(db_keys, vartree.dbapi.aux_get(cpv, db_keys)), |
| operation="uninstall", |
| root_config=root_config, |
| type_name="installed", |
| ) |
| pkg_cache[cpv] = pkg |
| return pkg |
| |
| vdb_path = os.path.join(settings["EROOT"], portage.VDB_PATH) |
| try: |
| # At least the parent needs to exist for the lock file. |
| portage.util.ensure_dirs(vdb_path) |
| except portage.exception.PortageException: |
| pass |
| vdb_lock = None |
| try: |
| if os.access(vdb_path, os.W_OK): |
| vartree.dbapi.lock() |
| vdb_lock = True |
| |
| realsyslist = [] |
| sys_virt_map = {} |
| for x in sets["system"].getAtoms(): |
| for atom in expand_new_virt(vartree.dbapi, x): |
| if not atom.blocker: |
| realsyslist.append(atom) |
| if atom.cp != x.cp: |
| sys_virt_map[atom.cp] = x.cp |
| |
| syslist = [] |
| for x in realsyslist: |
| mycp = x.cp |
| # Since Gentoo stopped using old-style virtuals in |
| # 2011, typically it's possible to avoid getvirtuals() |
| # calls entirely. It will not be triggered here by |
| # new-style virtuals since those are expanded to |
| # non-virtual atoms above by expand_new_virt(). |
| if mycp.startswith("virtual/") and mycp in settings.getvirtuals(): |
| providers = [] |
| for provider in settings.getvirtuals()[mycp]: |
| if vartree.dbapi.match(provider): |
| providers.append(provider) |
| if len(providers) == 1: |
| syslist.extend(providers) |
| else: |
| syslist.append(mycp) |
| syslist = frozenset(syslist) |
| |
| if not unmerge_files: |
| if unmerge_action in ["rage-clean", "unmerge"]: |
| print() |
| print( |
| bold("emerge %s" % unmerge_action) |
| + " can only be used with specific package names" |
| ) |
| print() |
| return 1, {} |
| |
| global_unmerge = 1 |
| |
| # process all arguments and add all |
| # valid db entries to candidate_catpkgs |
| if global_unmerge: |
| if not unmerge_files: |
| candidate_catpkgs.extend(vartree.dbapi.cp_all()) |
| else: |
| # we've got command-line arguments |
| if not unmerge_files: |
| print("\nNo packages to %s have been provided.\n" % unmerge_action) |
| return 1, {} |
| for x in unmerge_files: |
| arg_parts = x.split("/") |
| if x[0] not in [".", "/"] and arg_parts[-1][-7:] != ".ebuild": |
| # possible cat/pkg or dep; treat as such |
| candidate_catpkgs.append(x) |
| elif unmerge_action in ["prune", "clean"]: |
| print( |
| "\n!!! Prune and clean do not accept individual" |
| + " ebuilds as arguments;\n skipping.\n" |
| ) |
| continue |
| else: |
| # it appears that the user is specifying an installed |
| # ebuild and we're in "unmerge" mode, so it's ok. |
| if not os.path.exists(x): |
| print("\n!!! The path '" + x + "' doesn't exist.\n") |
| return 1, {} |
| |
| absx = os.path.abspath(x) |
| sp_absx = absx.split("/") |
| if sp_absx[-1][-7:] == ".ebuild": |
| del sp_absx[-1] |
| absx = "/".join(sp_absx) |
| |
| sp_absx_len = len(sp_absx) |
| |
| vdb_path = os.path.join(settings["EROOT"], portage.VDB_PATH) |
| |
| sp_vdb = vdb_path.split("/") |
| sp_vdb_len = len(sp_vdb) |
| |
| if not os.path.exists(absx + "/CONTENTS"): |
| print("!!! Not a valid db dir: " + str(absx)) |
| return 1, {} |
| |
| if sp_absx_len <= sp_vdb_len: |
| # The Path is shorter... so it can't be inside the vdb. |
| print(sp_absx) |
| print(absx) |
| print( |
| "\n!!!", x, "cannot be inside " + vdb_path + "; aborting.\n" |
| ) |
| return 1, {} |
| |
| for idx in range(0, sp_vdb_len): |
| if idx >= sp_absx_len or sp_vdb[idx] != sp_absx[idx]: |
| print(sp_absx) |
| print(absx) |
| print( |
| "\n!!!", |
| x, |
| "is not inside " + vdb_path + "; aborting.\n", |
| ) |
| return 1, {} |
| |
| print("=" + "/".join(sp_absx[sp_vdb_len:])) |
| candidate_catpkgs.append("=" + "/".join(sp_absx[sp_vdb_len:])) |
| |
| newline = "" |
| if not quiet: |
| newline = "\n" |
| if settings["ROOT"] != "/": |
| writemsg_level( |
| darkgreen( |
| newline |
| + ">>> Using system located in ROOT tree %s\n" % settings["ROOT"] |
| ) |
| ) |
| |
| if ("--pretend" in myopts or "--ask" in myopts) and not quiet: |
| writemsg_level( |
| darkgreen( |
| newline + ">>> These are the packages that would be unmerged:\n" |
| ) |
| ) |
| |
| # Preservation of order is required for --depclean and --prune so |
| # that dependencies are respected. Use all_selected to eliminate |
| # duplicate packages since the same package may be selected by |
| # multiple atoms. |
| pkgmap = [] |
| all_selected = set() |
| for x in candidate_catpkgs: |
| # cycle through all our candidate deps and determine |
| # what will and will not get unmerged |
| try: |
| mymatch = vartree.dbapi.match(x) |
| except portage.exception.AmbiguousPackageName as errpkgs: |
| print( |
| '\n\n!!! The short ebuild name "' |
| + x |
| + '" is ambiguous. Please specify' |
| ) |
| print( |
| "!!! one of the following fully-qualified " |
| + "ebuild names instead:\n" |
| ) |
| for i in errpkgs[0]: |
| print(" " + green(i)) |
| print() |
| sys.exit(1) |
| |
| if not mymatch and x[0] not in "<>=~": |
| mymatch = vartree.dep_match(x) |
| if not mymatch: |
| portage.writemsg( |
| "\n--- Couldn't find '%s' to %s.\n" |
| % (x.replace("null/", ""), unmerge_action), |
| noiselevel=-1, |
| ) |
| continue |
| |
| pkgmap.append({"protected": set(), "selected": set(), "omitted": set()}) |
| mykey = len(pkgmap) - 1 |
| if unmerge_action in ["rage-clean", "unmerge"]: |
| for y in mymatch: |
| if y not in all_selected: |
| pkgmap[mykey]["selected"].add(y) |
| all_selected.add(y) |
| elif unmerge_action == "prune": |
| if len(mymatch) == 1: |
| continue |
| best_version = mymatch[0] |
| best_slot = vartree.getslot(best_version) |
| best_counter = vartree.dbapi.cpv_counter(best_version) |
| for mypkg in mymatch[1:]: |
| myslot = vartree.getslot(mypkg) |
| mycounter = vartree.dbapi.cpv_counter(mypkg) |
| if ( |
| myslot == best_slot and mycounter > best_counter |
| ) or mypkg == portage.best([mypkg, best_version]): |
| if myslot == best_slot: |
| if mycounter < best_counter: |
| # On slot collision, keep the one with the |
| # highest counter since it is the most |
| # recently installed. |
| continue |
| best_version = mypkg |
| best_slot = myslot |
| best_counter = mycounter |
| pkgmap[mykey]["protected"].add(best_version) |
| pkgmap[mykey]["selected"].update( |
| mypkg |
| for mypkg in mymatch |
| if mypkg != best_version and mypkg not in all_selected |
| ) |
| all_selected.update(pkgmap[mykey]["selected"]) |
| else: |
| # unmerge_action == "clean" |
| slotmap = {} |
| for mypkg in mymatch: |
| if unmerge_action == "clean": |
| myslot = vartree.getslot(mypkg) |
| else: |
| # since we're pruning, we don't care about slots |
| # and put all the pkgs in together |
| myslot = 0 |
| if myslot not in slotmap: |
| slotmap[myslot] = {} |
| slotmap[myslot][vartree.dbapi.cpv_counter(mypkg)] = mypkg |
| |
| for mypkg in vartree.dbapi.cp_list(portage.cpv_getkey(mymatch[0])): |
| myslot = vartree.getslot(mypkg) |
| if myslot not in slotmap: |
| slotmap[myslot] = {} |
| slotmap[myslot][vartree.dbapi.cpv_counter(mypkg)] = mypkg |
| |
| for myslot in slotmap: |
| counterkeys = list(slotmap[myslot]) |
| if not counterkeys: |
| continue |
| counterkeys.sort() |
| pkgmap[mykey]["protected"].add(slotmap[myslot][counterkeys[-1]]) |
| del counterkeys[-1] |
| |
| for counter in counterkeys[:]: |
| mypkg = slotmap[myslot][counter] |
| if mypkg not in mymatch: |
| counterkeys.remove(counter) |
| pkgmap[mykey]["protected"].add(slotmap[myslot][counter]) |
| |
| # be pretty and get them in order of merge: |
| for ckey in counterkeys: |
| mypkg = slotmap[myslot][ckey] |
| if mypkg not in all_selected: |
| pkgmap[mykey]["selected"].add(mypkg) |
| all_selected.add(mypkg) |
| # ok, now the last-merged package |
| # is protected, and the rest are selected |
| numselected = len(all_selected) |
| if global_unmerge and not numselected: |
| portage.writemsg_stdout( |
| "\n>>> No outdated packages were found on your system.\n" |
| ) |
| return 1, {} |
| |
| if not numselected: |
| portage.writemsg_stdout( |
| "\n>>> No packages selected for removal by " + unmerge_action + "\n" |
| ) |
| return 1, {} |
| finally: |
| if vdb_lock: |
| vartree.dbapi.flush_cache() |
| vartree.dbapi.unlock() |
| |
| # generate a list of package sets that are directly or indirectly listed in "selected", |
| # as there is no persistent list of "installed" sets |
| installed_sets = ["selected"] |
| stop = False |
| pos = 0 |
| while not stop: |
| stop = True |
| pos = len(installed_sets) |
| for s in installed_sets[pos - 1 :]: |
| if s not in sets: |
| continue |
| candidates = [ |
| x[len(SETPREFIX) :] |
| for x in sets[s].getNonAtoms() |
| if x.startswith(SETPREFIX) |
| ] |
| if candidates: |
| stop = False |
| installed_sets += candidates |
| installed_sets = [ |
| x for x in installed_sets if x not in root_config.setconfig.active |
| ] |
| del stop, pos |
| |
| # we don't want to unmerge packages that are still listed in user-editable package sets |
| # listed in "world" as they would be remerged on the next update of "world" or the |
| # relevant package sets. |
| unknown_sets = set() |
| for cp in range(len(pkgmap)): |
| for cpv in pkgmap[cp]["selected"].copy(): |
| try: |
| pkg = _pkg(cpv) |
| except KeyError: |
| # It could have been uninstalled |
| # by a concurrent process. |
| continue |
| |
| if unmerge_action != "clean" and root_config.root == "/": |
| skip_pkg = False |
| if portage.match_from_list(portage.const.PORTAGE_PACKAGE_ATOM, [pkg]): |
| msg = ( |
| "Not unmerging package %s " |
| "since there is no valid reason for Portage to " |
| "%s itself." |
| ) % (pkg.cpv, unmerge_action) |
| skip_pkg = True |
| elif vartree.dbapi._dblink(cpv).isowner(portage._python_interpreter): |
| msg = ( |
| "Not unmerging package %s since there is no valid " |
| "reason for Portage to %s currently used Python " |
| "interpreter." |
| ) % (pkg.cpv, unmerge_action) |
| skip_pkg = True |
| if skip_pkg: |
| for line in textwrap.wrap(msg, 75): |
| out.eerror(line) |
| # adjust pkgmap so the display output is correct |
| pkgmap[cp]["selected"].remove(cpv) |
| all_selected.remove(cpv) |
| pkgmap[cp]["protected"].add(cpv) |
| continue |
| |
| parents = [] |
| for s in installed_sets: |
| # skip sets that the user requested to unmerge, and skip world |
| # user-selected set, since the package will be removed from |
| # that set later on. |
| if s in root_config.setconfig.active or s == "selected": |
| continue |
| |
| if s not in sets: |
| if s in unknown_sets: |
| continue |
| unknown_sets.add(s) |
| out = portage.output.EOutput() |
| out.eerror( |
| ("Unknown set '@%s' in %s%s") |
| % ( |
| s, |
| root_config.settings["EROOT"], |
| portage.const.WORLD_SETS_FILE, |
| ) |
| ) |
| continue |
| |
| # only check instances of EditablePackageSet as other classes are generally used for |
| # special purposes and can be ignored here (and are usually generated dynamically, so the |
| # user can't do much about them anyway) |
| if isinstance(sets[s], EditablePackageSet): |
| |
| # This is derived from a snippet of code in the |
| # depgraph._iter_atoms_for_pkg() method. |
| for atom in sets[s].iterAtomsForPackage(pkg): |
| inst_matches = vartree.dbapi.match(atom) |
| inst_matches.reverse() # descending order |
| higher_slot = None |
| for inst_cpv in inst_matches: |
| try: |
| inst_pkg = _pkg(inst_cpv) |
| except KeyError: |
| # It could have been uninstalled |
| # by a concurrent process. |
| continue |
| |
| if inst_pkg.cp != atom.cp: |
| continue |
| if pkg >= inst_pkg: |
| # This is descending order, and we're not |
| # interested in any versions <= pkg given. |
| break |
| if pkg.slot_atom != inst_pkg.slot_atom: |
| higher_slot = inst_pkg |
| break |
| if higher_slot is None: |
| parents.append(s) |
| break |
| if parents: |
| print(colorize("WARN", "Package %s is going to be unmerged," % cpv)) |
| print( |
| colorize("WARN", "but still listed in the following package sets:") |
| ) |
| print(" %s\n" % ", ".join(parents)) |
| |
| del installed_sets |
| |
| numselected = len(all_selected) |
| if not numselected: |
| writemsg_level( |
| "\n>>> No packages selected for removal by " + unmerge_action + "\n" |
| ) |
| return 1, {} |
| |
| # Unmerge order only matters in some cases |
| if not ordered: |
| unordered = {} |
| for d in pkgmap: |
| selected = d["selected"] |
| if not selected: |
| continue |
| cp = portage.cpv_getkey(next(iter(selected))) |
| cp_dict = unordered.get(cp) |
| if cp_dict is None: |
| cp_dict = {} |
| unordered[cp] = cp_dict |
| for k in d: |
| cp_dict[k] = set() |
| for k, v in d.items(): |
| cp_dict[k].update(v) |
| pkgmap = [unordered[cp] for cp in sorted(unordered)] |
| |
| # Sort each set of selected packages |
| if ordered: |
| for pkg in pkgmap: |
| pkg["selected"] = sorted(pkg["selected"], key=cpv_sort_key()) |
| |
| for x in range(len(pkgmap)): |
| selected = pkgmap[x]["selected"] |
| if not selected: |
| continue |
| for mytype, mylist in pkgmap[x].items(): |
| if mytype == "selected": |
| continue |
| mylist.difference_update(all_selected) |
| cp = portage.cpv_getkey(next(iter(selected))) |
| for y in vartree.dep_match(cp): |
| if ( |
| y not in pkgmap[x]["omitted"] |
| and y not in pkgmap[x]["selected"] |
| and y not in pkgmap[x]["protected"] |
| and y not in all_selected |
| ): |
| pkgmap[x]["omitted"].add(y) |
| if global_unmerge and not pkgmap[x]["selected"]: |
| # avoid cluttering the preview printout with stuff that isn't getting unmerged |
| continue |
| if not (pkgmap[x]["protected"] or pkgmap[x]["omitted"]) and cp in syslist: |
| virt_cp = sys_virt_map.get(cp) |
| if virt_cp is None: |
| cp_info = "'%s'" % (cp,) |
| else: |
| cp_info = "'%s' (%s)" % (cp, virt_cp) |
| writemsg_level( |
| colorize( |
| "BAD", |
| "\n\n!!! " + "%s is part of your system profile.\n" % (cp_info,), |
| ), |
| level=logging.WARNING, |
| noiselevel=-1, |
| ) |
| writemsg_level( |
| colorize( |
| "WARN", "!!! Unmerging it may " + "be damaging to your system.\n\n" |
| ), |
| level=logging.WARNING, |
| noiselevel=-1, |
| ) |
| if not quiet: |
| writemsg_level("\n %s\n" % (bold(cp),), noiselevel=-1) |
| else: |
| writemsg_level(bold(cp) + ": ", noiselevel=-1) |
| for mytype in ["selected", "protected", "omitted"]: |
| if not quiet: |
| writemsg_level((mytype + ": ").rjust(14), noiselevel=-1) |
| if pkgmap[x][mytype]: |
| sorted_pkgs = [] |
| for mypkg in pkgmap[x][mytype]: |
| try: |
| sorted_pkgs.append(mypkg.cpv) |
| except AttributeError: |
| sorted_pkgs.append(_pkg_str(mypkg)) |
| sorted_pkgs.sort(key=cpv_sort_key()) |
| for mypkg in sorted_pkgs: |
| if mytype == "selected": |
| writemsg_level( |
| colorize("UNMERGE_WARN", mypkg.version + " "), noiselevel=-1 |
| ) |
| else: |
| writemsg_level( |
| colorize("GOOD", mypkg.version + " "), noiselevel=-1 |
| ) |
| else: |
| writemsg_level("none ", noiselevel=-1) |
| if not quiet: |
| writemsg_level("\n", noiselevel=-1) |
| if quiet: |
| writemsg_level("\n", noiselevel=-1) |
| |
| writemsg_level( |
| "\nAll selected packages: %s\n" % " ".join("=%s" % x for x in all_selected), |
| noiselevel=-1, |
| ) |
| |
| writemsg_level( |
| "\n>>> " |
| + colorize("UNMERGE_WARN", "'Selected'") |
| + " packages are slated for removal.\n" |
| ) |
| writemsg_level( |
| ">>> " |
| + colorize("GOOD", "'Protected'") |
| + " and " |
| + colorize("GOOD", "'omitted'") |
| + " packages will not be removed.\n\n" |
| ) |
| |
| return os.EX_OK, pkgmap |
| |
| |
| def unmerge( |
| root_config, |
| myopts, |
| unmerge_action, |
| unmerge_files, |
| ldpath_mtimes, |
| autoclean=0, |
| clean_world=1, |
| clean_delay=1, |
| ordered=0, |
| raise_on_error=0, |
| scheduler=None, |
| writemsg_level=portage.util.writemsg_level, |
| ): |
| """ |
| Returns os.EX_OK if no errors occur, 1 if an error occurs, and |
| 130 if interrupted due to a 'no' answer for --ask. |
| """ |
| |
| if clean_world: |
| clean_world = myopts.get("--deselect") != "n" |
| |
| rval, pkgmap = _unmerge_display( |
| root_config, |
| myopts, |
| unmerge_action, |
| unmerge_files, |
| clean_delay=clean_delay, |
| ordered=ordered, |
| writemsg_level=writemsg_level, |
| ) |
| |
| if rval != os.EX_OK: |
| return rval |
| |
| enter_invalid = "--ask-enter-invalid" in myopts |
| vartree = root_config.trees["vartree"] |
| sets = root_config.sets |
| settings = root_config.settings |
| mysettings = portage.config(clone=settings) |
| xterm_titles = "notitles" not in settings.features |
| |
| if "--pretend" in myopts: |
| # we're done... return |
| return os.EX_OK |
| if "--ask" in myopts: |
| uq = UserQuery(myopts) |
| if uq.query("Would you like to unmerge these packages?", enter_invalid) == "No": |
| # enter pretend mode for correct formatting of results |
| myopts["--pretend"] = True |
| print() |
| print("Quitting.") |
| print() |
| return 128 + signal.SIGINT |
| |
| if not vartree.dbapi.writable: |
| writemsg_level( |
| "!!! %s\n" % _("Read-only file system: %s") % vartree.dbapi._dbroot, |
| level=logging.ERROR, |
| noiselevel=-1, |
| ) |
| return 1 |
| |
| # the real unmerging begins, after a short delay unless we're raging.... |
| if not unmerge_action == "rage-clean" and clean_delay and not autoclean: |
| countdown(int(settings["CLEAN_DELAY"]), ">>> Unmerging") |
| |
| all_selected = set() |
| all_selected.update(*[x["selected"] for x in pkgmap]) |
| |
| # Set counter variables |
| curval = 1 |
| maxval = len(all_selected) |
| |
| for x in range(len(pkgmap)): |
| for y in pkgmap[x]["selected"]: |
| emergelog(xterm_titles, "=== Unmerging... (" + y + ")") |
| message = ">>> Unmerging ({0} of {1}) {2}...\n".format( |
| colorize("MERGE_LIST_PROGRESS", str(curval)), |
| colorize("MERGE_LIST_PROGRESS", str(maxval)), |
| y, |
| ) |
| writemsg_level(message, noiselevel=-1) |
| curval += 1 |
| |
| mysplit = y.split("/") |
| # unmerge... |
| retval = portage.unmerge( |
| mysplit[0], |
| mysplit[1], |
| settings=mysettings, |
| vartree=vartree, |
| ldpath_mtimes=ldpath_mtimes, |
| scheduler=scheduler, |
| ) |
| |
| if retval != os.EX_OK: |
| emergelog(xterm_titles, " !!! unmerge FAILURE: " + y) |
| if raise_on_error: |
| raise UninstallFailure(retval) |
| sys.exit(retval) |
| else: |
| if ( |
| clean_world |
| and hasattr(sets["selected"], "cleanPackage") |
| and hasattr(sets["selected"], "lock") |
| ): |
| sets["selected"].lock() |
| if hasattr(sets["selected"], "load"): |
| sets["selected"].load() |
| sets["selected"].cleanPackage(vartree.dbapi, y) |
| sets["selected"].unlock() |
| emergelog(xterm_titles, " >>> unmerge success: " + y) |
| |
| if ( |
| clean_world |
| and hasattr(sets["selected"], "remove") |
| and hasattr(sets["selected"], "lock") |
| ): |
| sets["selected"].lock() |
| # load is called inside remove() |
| for s in root_config.setconfig.active: |
| sets["selected"].remove(SETPREFIX + s) |
| sets["selected"].unlock() |
| |
| return os.EX_OK |