blob: 8923e20ea45e76036327b133e79d32f805e94d2e [file] [log] [blame]
# Copyright 1999-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
from __future__ import print_function
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, {}
else:
global_unmerge = 1
localtree = vartree
# 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" in myopts):
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" in myopts):
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 = localtree.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 = localtree.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][localtree.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)]
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 localtree.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