emerge --sync: support repo arguments
[portage.git] / pym / _emerge / unmerge.py
1 # Copyright 1999-2012 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3
4 from __future__ import print_function
5
6 import logging
7 import signal
8 import sys
9 import textwrap
10 import portage
11 from portage import os
12 from portage.dbapi._expand_new_virt import expand_new_virt
13 from portage.output import bold, colorize, darkgreen, green
14 from portage._sets import SETPREFIX
15 from portage._sets.base import EditablePackageSet
16 from portage.versions import cpv_sort_key, _pkg_str
17
18 from _emerge.emergelog import emergelog
19 from _emerge.Package import Package
20 from _emerge.UninstallFailure import UninstallFailure
21 from _emerge.userquery import userquery
22 from _emerge.countdown import countdown
23
24 def _unmerge_display(root_config, myopts, unmerge_action,
25         unmerge_files, clean_delay=1, ordered=0,
26         writemsg_level=portage.util.writemsg_level):
27         """
28         Returns a tuple of (returncode, pkgmap) where returncode is
29         os.EX_OK if no errors occur, and 1 otherwise.
30         """
31
32         quiet = "--quiet" in myopts
33         settings = root_config.settings
34         sets = root_config.sets
35         vartree = root_config.trees["vartree"]
36         candidate_catpkgs=[]
37         global_unmerge=0
38         out = portage.output.EOutput()
39         pkg_cache = {}
40         db_keys = list(vartree.dbapi._aux_cache_keys)
41
42         def _pkg(cpv):
43                 pkg = pkg_cache.get(cpv)
44                 if pkg is None:
45                         pkg = Package(built=True, cpv=cpv, installed=True,
46                                 metadata=zip(db_keys, vartree.dbapi.aux_get(cpv, db_keys)),
47                                 operation="uninstall", root_config=root_config,
48                                 type_name="installed")
49                         pkg_cache[cpv] = pkg
50                 return pkg
51
52         vdb_path = os.path.join(settings["EROOT"], portage.VDB_PATH)
53         try:
54                 # At least the parent needs to exist for the lock file.
55                 portage.util.ensure_dirs(vdb_path)
56         except portage.exception.PortageException:
57                 pass
58         vdb_lock = None
59         try:
60                 if os.access(vdb_path, os.W_OK):
61                         vartree.dbapi.lock()
62                         vdb_lock = True
63
64                 realsyslist = []
65                 sys_virt_map = {}
66                 for x in sets["system"].getAtoms():
67                         for atom in expand_new_virt(vartree.dbapi, x):
68                                 if not atom.blocker:
69                                         realsyslist.append(atom)
70                                         if atom.cp != x.cp:
71                                                 sys_virt_map[atom.cp] = x.cp
72
73                 syslist = []
74                 for x in realsyslist:
75                         mycp = x.cp
76                         # Since Gentoo stopped using old-style virtuals in
77                         # 2011, typically it's possible to avoid getvirtuals()
78                         # calls entirely. It will not be triggered here by
79                         # new-style virtuals since those are expanded to
80                         # non-virtual atoms above by expand_new_virt().
81                         if mycp.startswith("virtual/") and \
82                                 mycp in settings.getvirtuals():
83                                 providers = []
84                                 for provider in settings.getvirtuals()[mycp]:
85                                         if vartree.dbapi.match(provider):
86                                                 providers.append(provider)
87                                 if len(providers) == 1:
88                                         syslist.extend(providers)
89                         else:
90                                 syslist.append(mycp)
91                 syslist = frozenset(syslist)
92         
93                 if not unmerge_files:
94                         if unmerge_action == "unmerge":
95                                 print()
96                                 print(bold("emerge unmerge") + " can only be used with specific package names")
97                                 print()
98                                 return 1, {}
99                         else:
100                                 global_unmerge = 1
101         
102                 localtree = vartree
103                 # process all arguments and add all
104                 # valid db entries to candidate_catpkgs
105                 if global_unmerge:
106                         if not unmerge_files:
107                                 candidate_catpkgs.extend(vartree.dbapi.cp_all())
108                 else:
109                         #we've got command-line arguments
110                         if not unmerge_files:
111                                 print("\nNo packages to unmerge have been provided.\n")
112                                 return 1, {}
113                         for x in unmerge_files:
114                                 arg_parts = x.split('/')
115                                 if x[0] not in [".","/"] and \
116                                         arg_parts[-1][-7:] != ".ebuild":
117                                         #possible cat/pkg or dep; treat as such
118                                         candidate_catpkgs.append(x)
119                                 elif unmerge_action in ["prune","clean"]:
120                                         print("\n!!! Prune and clean do not accept individual" + \
121                                                 " ebuilds as arguments;\n    skipping.\n")
122                                         continue
123                                 else:
124                                         # it appears that the user is specifying an installed
125                                         # ebuild and we're in "unmerge" mode, so it's ok.
126                                         if not os.path.exists(x):
127                                                 print("\n!!! The path '"+x+"' doesn't exist.\n")
128                                                 return 1, {}
129         
130                                         absx   = os.path.abspath(x)
131                                         sp_absx = absx.split("/")
132                                         if sp_absx[-1][-7:] == ".ebuild":
133                                                 del sp_absx[-1]
134                                                 absx = "/".join(sp_absx)
135         
136                                         sp_absx_len = len(sp_absx)
137         
138                                         vdb_path = os.path.join(settings["EROOT"], portage.VDB_PATH)
139         
140                                         sp_vdb     = vdb_path.split("/")
141                                         sp_vdb_len = len(sp_vdb)
142         
143                                         if not os.path.exists(absx+"/CONTENTS"):
144                                                 print("!!! Not a valid db dir: "+str(absx))
145                                                 return 1, {}
146         
147                                         if sp_absx_len <= sp_vdb_len:
148                                                 # The Path is shorter... so it can't be inside the vdb.
149                                                 print(sp_absx)
150                                                 print(absx)
151                                                 print("\n!!!",x,"cannot be inside "+ \
152                                                         vdb_path+"; aborting.\n")
153                                                 return 1, {}
154         
155                                         for idx in range(0,sp_vdb_len):
156                                                 if idx >= sp_absx_len or sp_vdb[idx] != sp_absx[idx]:
157                                                         print(sp_absx)
158                                                         print(absx)
159                                                         print("\n!!!", x, "is not inside "+\
160                                                                 vdb_path+"; aborting.\n")
161                                                         return 1, {}
162         
163                                         print("="+"/".join(sp_absx[sp_vdb_len:]))
164                                         candidate_catpkgs.append(
165                                                 "="+"/".join(sp_absx[sp_vdb_len:]))
166         
167                 newline=""
168                 if (not "--quiet" in myopts):
169                         newline="\n"
170                 if settings["ROOT"] != "/":
171                         writemsg_level(darkgreen(newline+ \
172                                 ">>> Using system located in ROOT tree %s\n" % \
173                                 settings["ROOT"]))
174
175                 if (("--pretend" in myopts) or ("--ask" in myopts)) and \
176                         not ("--quiet" in myopts):
177                         writemsg_level(darkgreen(newline+\
178                                 ">>> These are the packages that would be unmerged:\n"))
179
180                 # Preservation of order is required for --depclean and --prune so
181                 # that dependencies are respected. Use all_selected to eliminate
182                 # duplicate packages since the same package may be selected by
183                 # multiple atoms.
184                 pkgmap = []
185                 all_selected = set()
186                 for x in candidate_catpkgs:
187                         # cycle through all our candidate deps and determine
188                         # what will and will not get unmerged
189                         try:
190                                 mymatch = vartree.dbapi.match(x)
191                         except portage.exception.AmbiguousPackageName as errpkgs:
192                                 print("\n\n!!! The short ebuild name \"" + \
193                                         x + "\" is ambiguous.  Please specify")
194                                 print("!!! one of the following fully-qualified " + \
195                                         "ebuild names instead:\n")
196                                 for i in errpkgs[0]:
197                                         print("    " + green(i))
198                                 print()
199                                 sys.exit(1)
200         
201                         if not mymatch and x[0] not in "<>=~":
202                                 mymatch = localtree.dep_match(x)
203                         if not mymatch:
204                                 portage.writemsg("\n--- Couldn't find '%s' to %s.\n" % \
205                                         (x.replace("null/", ""), unmerge_action), noiselevel=-1)
206                                 continue
207
208                         pkgmap.append(
209                                 {"protected": set(), "selected": set(), "omitted": set()})
210                         mykey = len(pkgmap) - 1
211                         if unmerge_action=="unmerge":
212                                         for y in mymatch:
213                                                 if y not in all_selected:
214                                                         pkgmap[mykey]["selected"].add(y)
215                                                         all_selected.add(y)
216                         elif unmerge_action == "prune":
217                                 if len(mymatch) == 1:
218                                         continue
219                                 best_version = mymatch[0]
220                                 best_slot = vartree.getslot(best_version)
221                                 best_counter = vartree.dbapi.cpv_counter(best_version)
222                                 for mypkg in mymatch[1:]:
223                                         myslot = vartree.getslot(mypkg)
224                                         mycounter = vartree.dbapi.cpv_counter(mypkg)
225                                         if (myslot == best_slot and mycounter > best_counter) or \
226                                                 mypkg == portage.best([mypkg, best_version]):
227                                                 if myslot == best_slot:
228                                                         if mycounter < best_counter:
229                                                                 # On slot collision, keep the one with the
230                                                                 # highest counter since it is the most
231                                                                 # recently installed.
232                                                                 continue
233                                                 best_version = mypkg
234                                                 best_slot = myslot
235                                                 best_counter = mycounter
236                                 pkgmap[mykey]["protected"].add(best_version)
237                                 pkgmap[mykey]["selected"].update(mypkg for mypkg in mymatch \
238                                         if mypkg != best_version and mypkg not in all_selected)
239                                 all_selected.update(pkgmap[mykey]["selected"])
240                         else:
241                                 # unmerge_action == "clean"
242                                 slotmap={}
243                                 for mypkg in mymatch:
244                                         if unmerge_action == "clean":
245                                                 myslot = localtree.getslot(mypkg)
246                                         else:
247                                                 # since we're pruning, we don't care about slots
248                                                 # and put all the pkgs in together
249                                                 myslot = 0
250                                         if myslot not in slotmap:
251                                                 slotmap[myslot] = {}
252                                         slotmap[myslot][localtree.dbapi.cpv_counter(mypkg)] = mypkg
253
254                                 for mypkg in vartree.dbapi.cp_list(
255                                         portage.cpv_getkey(mymatch[0])):
256                                         myslot = vartree.getslot(mypkg)
257                                         if myslot not in slotmap:
258                                                 slotmap[myslot] = {}
259                                         slotmap[myslot][vartree.dbapi.cpv_counter(mypkg)] = mypkg
260
261                                 for myslot in slotmap:
262                                         counterkeys = list(slotmap[myslot])
263                                         if not counterkeys:
264                                                 continue
265                                         counterkeys.sort()
266                                         pkgmap[mykey]["protected"].add(
267                                                 slotmap[myslot][counterkeys[-1]])
268                                         del counterkeys[-1]
269
270                                         for counter in counterkeys[:]:
271                                                 mypkg = slotmap[myslot][counter]
272                                                 if mypkg not in mymatch:
273                                                         counterkeys.remove(counter)
274                                                         pkgmap[mykey]["protected"].add(
275                                                                 slotmap[myslot][counter])
276
277                                         #be pretty and get them in order of merge:
278                                         for ckey in counterkeys:
279                                                 mypkg = slotmap[myslot][ckey]
280                                                 if mypkg not in all_selected:
281                                                         pkgmap[mykey]["selected"].add(mypkg)
282                                                         all_selected.add(mypkg)
283                                         # ok, now the last-merged package
284                                         # is protected, and the rest are selected
285                 numselected = len(all_selected)
286                 if global_unmerge and not numselected:
287                         portage.writemsg_stdout("\n>>> No outdated packages were found on your system.\n")
288                         return 1, {}
289         
290                 if not numselected:
291                         portage.writemsg_stdout(
292                                 "\n>>> No packages selected for removal by " + \
293                                 unmerge_action + "\n")
294                         return 1, {}
295         finally:
296                 if vdb_lock:
297                         vartree.dbapi.flush_cache()
298                         vartree.dbapi.unlock()
299
300         # generate a list of package sets that are directly or indirectly listed in "selected",
301         # as there is no persistent list of "installed" sets
302         installed_sets = ["selected"]
303         stop = False
304         pos = 0
305         while not stop:
306                 stop = True
307                 pos = len(installed_sets)
308                 for s in installed_sets[pos - 1:]:
309                         if s not in sets:
310                                 continue
311                         candidates = [x[len(SETPREFIX):] for x in sets[s].getNonAtoms() if x.startswith(SETPREFIX)]
312                         if candidates:
313                                 stop = False
314                                 installed_sets += candidates
315         installed_sets = [x for x in installed_sets if x not in root_config.setconfig.active]
316         del stop, pos
317
318         # we don't want to unmerge packages that are still listed in user-editable package sets
319         # listed in "world" as they would be remerged on the next update of "world" or the 
320         # relevant package sets.
321         unknown_sets = set()
322         for cp in range(len(pkgmap)):
323                 for cpv in pkgmap[cp]["selected"].copy():
324                         try:
325                                 pkg = _pkg(cpv)
326                         except KeyError:
327                                 # It could have been uninstalled
328                                 # by a concurrent process.
329                                 continue
330
331                         if unmerge_action != "clean" and root_config.root == "/":
332                                 skip_pkg = False
333                                 if portage.match_from_list(portage.const.PORTAGE_PACKAGE_ATOM, [pkg]):
334                                         msg = ("Not unmerging package %s since there is no valid reason "
335                                                 "for Portage to unmerge itself.") % (pkg.cpv,)
336                                         skip_pkg = True
337                                 elif vartree.dbapi._dblink(cpv).isowner(portage._python_interpreter):
338                                         msg = ("Not unmerging package %s since there is no valid reason "
339                                                 "for Portage to unmerge currently used Python interpreter.") % (pkg.cpv,)
340                                         skip_pkg = True
341                                 if skip_pkg:
342                                         for line in textwrap.wrap(msg, 75):
343                                                 out.eerror(line)
344                                         # adjust pkgmap so the display output is correct
345                                         pkgmap[cp]["selected"].remove(cpv)
346                                         all_selected.remove(cpv)
347                                         pkgmap[cp]["protected"].add(cpv)
348                                         continue
349
350                         parents = []
351                         for s in installed_sets:
352                                 # skip sets that the user requested to unmerge, and skip world 
353                                 # user-selected set, since the package will be removed from
354                                 # that set later on.
355                                 if s in root_config.setconfig.active or s == "selected":
356                                         continue
357
358                                 if s not in sets:
359                                         if s in unknown_sets:
360                                                 continue
361                                         unknown_sets.add(s)
362                                         out = portage.output.EOutput()
363                                         out.eerror(("Unknown set '@%s' in %s%s") % \
364                                                 (s, root_config.settings['EROOT'], portage.const.WORLD_SETS_FILE))
365                                         continue
366
367                                 # only check instances of EditablePackageSet as other classes are generally used for
368                                 # special purposes and can be ignored here (and are usually generated dynamically, so the
369                                 # user can't do much about them anyway)
370                                 if isinstance(sets[s], EditablePackageSet):
371
372                                         # This is derived from a snippet of code in the
373                                         # depgraph._iter_atoms_for_pkg() method.
374                                         for atom in sets[s].iterAtomsForPackage(pkg):
375                                                 inst_matches = vartree.dbapi.match(atom)
376                                                 inst_matches.reverse() # descending order
377                                                 higher_slot = None
378                                                 for inst_cpv in inst_matches:
379                                                         try:
380                                                                 inst_pkg = _pkg(inst_cpv)
381                                                         except KeyError:
382                                                                 # It could have been uninstalled
383                                                                 # by a concurrent process.
384                                                                 continue
385
386                                                         if inst_pkg.cp != atom.cp:
387                                                                 continue
388                                                         if pkg >= inst_pkg:
389                                                                 # This is descending order, and we're not
390                                                                 # interested in any versions <= pkg given.
391                                                                 break
392                                                         if pkg.slot_atom != inst_pkg.slot_atom:
393                                                                 higher_slot = inst_pkg
394                                                                 break
395                                                 if higher_slot is None:
396                                                         parents.append(s)
397                                                         break
398                         if parents:
399                                 print(colorize("WARN", "Package %s is going to be unmerged," % cpv))
400                                 print(colorize("WARN", "but still listed in the following package sets:"))
401                                 print("    %s\n" % ", ".join(parents))
402
403         del installed_sets
404
405         numselected = len(all_selected)
406         if not numselected:
407                 writemsg_level(
408                         "\n>>> No packages selected for removal by " + \
409                         unmerge_action + "\n")
410                 return 1, {}
411
412         # Unmerge order only matters in some cases
413         if not ordered:
414                 unordered = {}
415                 for d in pkgmap:
416                         selected = d["selected"]
417                         if not selected:
418                                 continue
419                         cp = portage.cpv_getkey(next(iter(selected)))
420                         cp_dict = unordered.get(cp)
421                         if cp_dict is None:
422                                 cp_dict = {}
423                                 unordered[cp] = cp_dict
424                                 for k in d:
425                                         cp_dict[k] = set()
426                         for k, v in d.items():
427                                 cp_dict[k].update(v)
428                 pkgmap = [unordered[cp] for cp in sorted(unordered)]
429
430         for x in range(len(pkgmap)):
431                 selected = pkgmap[x]["selected"]
432                 if not selected:
433                         continue
434                 for mytype, mylist in pkgmap[x].items():
435                         if mytype == "selected":
436                                 continue
437                         mylist.difference_update(all_selected)
438                 cp = portage.cpv_getkey(next(iter(selected)))
439                 for y in localtree.dep_match(cp):
440                         if y not in pkgmap[x]["omitted"] and \
441                                 y not in pkgmap[x]["selected"] and \
442                                 y not in pkgmap[x]["protected"] and \
443                                 y not in all_selected:
444                                 pkgmap[x]["omitted"].add(y)
445                 if global_unmerge and not pkgmap[x]["selected"]:
446                         #avoid cluttering the preview printout with stuff that isn't getting unmerged
447                         continue
448                 if not (pkgmap[x]["protected"] or pkgmap[x]["omitted"]) and cp in syslist:
449                         virt_cp = sys_virt_map.get(cp)
450                         if virt_cp is None:
451                                 cp_info = "'%s'" % (cp,)
452                         else:
453                                 cp_info = "'%s' (%s)" % (cp, virt_cp)
454                         writemsg_level(colorize("BAD","\n\n!!! " + \
455                                 "%s is part of your system profile.\n" % (cp_info,)),
456                                 level=logging.WARNING, noiselevel=-1)
457                         writemsg_level(colorize("WARN","!!! Unmerging it may " + \
458                                 "be damaging to your system.\n\n"),
459                                 level=logging.WARNING, noiselevel=-1)
460                         if clean_delay and "--pretend" not in myopts and "--ask" not in myopts:
461                                 countdown(int(settings["EMERGE_WARNING_DELAY"]),
462                                         colorize("UNMERGE_WARN", "Press Ctrl-C to Stop"))
463                 if not quiet:
464                         writemsg_level("\n %s\n" % (bold(cp),), noiselevel=-1)
465                 else:
466                         writemsg_level(bold(cp) + ": ", noiselevel=-1)
467                 for mytype in ["selected","protected","omitted"]:
468                         if not quiet:
469                                 writemsg_level((mytype + ": ").rjust(14), noiselevel=-1)
470                         if pkgmap[x][mytype]:
471                                 sorted_pkgs = []
472                                 for mypkg in pkgmap[x][mytype]:
473                                         try:
474                                                 sorted_pkgs.append(mypkg.cpv)
475                                         except AttributeError:
476                                                 sorted_pkgs.append(_pkg_str(mypkg))
477                                 sorted_pkgs.sort(key=cpv_sort_key())
478                                 for mypkg in sorted_pkgs:
479                                         if mytype == "selected":
480                                                 writemsg_level(
481                                                         colorize("UNMERGE_WARN", mypkg.version + " "),
482                                                         noiselevel=-1)
483                                         else:
484                                                 writemsg_level(
485                                                         colorize("GOOD", mypkg.version + " "),
486                                                         noiselevel=-1)
487                         else:
488                                 writemsg_level("none ", noiselevel=-1)
489                         if not quiet:
490                                 writemsg_level("\n", noiselevel=-1)
491                 if quiet:
492                         writemsg_level("\n", noiselevel=-1)
493
494         writemsg_level("\nAll selected packages: %s\n" % " ".join(all_selected), noiselevel=-1)
495
496         writemsg_level("\n>>> " + colorize("UNMERGE_WARN", "'Selected'") + \
497                 " packages are slated for removal.\n")
498         writemsg_level(">>> " + colorize("GOOD", "'Protected'") + \
499                         " and " + colorize("GOOD", "'omitted'") + \
500                         " packages will not be removed.\n\n")
501
502         return os.EX_OK, pkgmap
503
504 def unmerge(root_config, myopts, unmerge_action,
505         unmerge_files, ldpath_mtimes, autoclean=0,
506         clean_world=1, clean_delay=1, ordered=0, raise_on_error=0,
507         scheduler=None, writemsg_level=portage.util.writemsg_level):
508         """
509         Returns os.EX_OK if no errors occur, 1 if an error occurs, and
510         130 if interrupted due to a 'no' answer for --ask.
511         """
512
513         if clean_world:
514                 clean_world = myopts.get('--deselect') != 'n'
515
516         rval, pkgmap = _unmerge_display(root_config, myopts,
517                 unmerge_action, unmerge_files,
518                 clean_delay=clean_delay, ordered=ordered,
519                 writemsg_level=writemsg_level)
520
521         if rval != os.EX_OK:
522                 return rval
523
524         enter_invalid = '--ask-enter-invalid' in myopts
525         vartree = root_config.trees["vartree"]
526         sets = root_config.sets
527         settings = root_config.settings
528         mysettings = portage.config(clone=settings)
529         xterm_titles = "notitles" not in settings.features
530
531         if "--pretend" in myopts:
532                 #we're done... return
533                 return os.EX_OK
534         if "--ask" in myopts:
535                 if userquery("Would you like to unmerge these packages?",
536                         enter_invalid) == "No":
537                         # enter pretend mode for correct formatting of results
538                         myopts["--pretend"] = True
539                         print()
540                         print("Quitting.")
541                         print()
542                         return 128 + signal.SIGINT
543         #the real unmerging begins, after a short delay....
544         if clean_delay and not autoclean:
545                 countdown(int(settings["CLEAN_DELAY"]), ">>> Unmerging")
546
547         all_selected = set()
548         all_selected.update(*[x["selected"] for x in pkgmap])
549
550         # Set counter variables
551         curval = 1
552         maxval = len(all_selected)
553
554         for x in range(len(pkgmap)):
555                 for y in pkgmap[x]["selected"]:
556                         emergelog(xterm_titles, "=== Unmerging... ("+y+")")
557                         message = ">>> Unmerging ({0} of {1}) {2}...\n".format(
558                                 colorize("MERGE_LIST_PROGRESS", str(curval)),
559                                 colorize("MERGE_LIST_PROGRESS", str(maxval)),
560                                 y)
561                         writemsg_level(message, noiselevel=-1)
562                         curval += 1
563
564                         mysplit = y.split("/")
565                         #unmerge...
566                         retval = portage.unmerge(mysplit[0], mysplit[1],
567                                 settings=mysettings,
568                                 vartree=vartree, ldpath_mtimes=ldpath_mtimes,
569                                 scheduler=scheduler)
570
571                         if retval != os.EX_OK:
572                                 emergelog(xterm_titles, " !!! unmerge FAILURE: "+y)
573                                 if raise_on_error:
574                                         raise UninstallFailure(retval)
575                                 sys.exit(retval)
576                         else:
577                                 if clean_world and hasattr(sets["selected"], "cleanPackage")\
578                                                 and hasattr(sets["selected"], "lock"):
579                                         sets["selected"].lock()
580                                         if hasattr(sets["selected"], "load"):
581                                                 sets["selected"].load()
582                                         sets["selected"].cleanPackage(vartree.dbapi, y)
583                                         sets["selected"].unlock()
584                                 emergelog(xterm_titles, " >>> unmerge success: "+y)
585
586         if clean_world and hasattr(sets["selected"], "remove")\
587                         and hasattr(sets["selected"], "lock"):
588                 sets["selected"].lock()
589                 # load is called inside remove()
590                 for s in root_config.setconfig.active:
591                         sets["selected"].remove(SETPREFIX + s)
592                 sets["selected"].unlock()
593
594         return os.EX_OK
595